From 353e50a9839b8563f9f9ab2d30a26c7764369351 Mon Sep 17 00:00:00 2001 From: triompha Date: Tue, 24 Dec 2013 18:11:32 +0800 Subject: [PATCH 1/4] code submit --- .../memcached/ByteBufArrayInputStream.java | 136 - .../memcached/ContextObjectInputStream.java | 44 - src/com/meetup/memcached/ErrorHandler.java | 72 - src/com/meetup/memcached/LineInputStream.java | 43 - src/com/meetup/memcached/Logger.java | 202 -- src/com/meetup/memcached/MemcachedClient.java | 2316 ----------------- src/com/meetup/memcached/NativeHandler.java | 443 ---- .../meetup/memcached/NestedIOException.java | 44 - src/com/meetup/memcached/SockIOPool.java | 1903 -------------- .../meetup/memcached/test/MemcachedBench.java | 99 - .../meetup/memcached/test/MemcachedTest.java | 151 -- .../meetup/memcached/test/TestMemcached.java | 63 - src/com/meetup/memcached/test/UnitTests.java | 403 --- 13 files changed, 5919 deletions(-) delete mode 100644 src/com/meetup/memcached/ByteBufArrayInputStream.java delete mode 100644 src/com/meetup/memcached/ContextObjectInputStream.java delete mode 100644 src/com/meetup/memcached/ErrorHandler.java delete mode 100644 src/com/meetup/memcached/LineInputStream.java delete mode 100644 src/com/meetup/memcached/Logger.java delete mode 100644 src/com/meetup/memcached/MemcachedClient.java delete mode 100644 src/com/meetup/memcached/NativeHandler.java delete mode 100644 src/com/meetup/memcached/NestedIOException.java delete mode 100644 src/com/meetup/memcached/SockIOPool.java delete mode 100644 src/com/meetup/memcached/test/MemcachedBench.java delete mode 100644 src/com/meetup/memcached/test/MemcachedTest.java delete mode 100644 src/com/meetup/memcached/test/TestMemcached.java delete mode 100644 src/com/meetup/memcached/test/UnitTests.java diff --git a/src/com/meetup/memcached/ByteBufArrayInputStream.java b/src/com/meetup/memcached/ByteBufArrayInputStream.java deleted file mode 100644 index c12caed..0000000 --- a/src/com/meetup/memcached/ByteBufArrayInputStream.java +++ /dev/null @@ -1,136 +0,0 @@ -/** - * 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 - */ -package com.meetup.memcached; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.util.ArrayList; -import java.util.List; - -public final class ByteBufArrayInputStream extends InputStream implements LineInputStream { - private ByteBuffer[] bufs; - private int currentBuf = 0; - - public ByteBufArrayInputStream( List bufs ) throws Exception { - this( bufs.toArray( new ByteBuffer[] {} ) ); - } - - public ByteBufArrayInputStream( ByteBuffer[] bufs ) throws Exception { - if ( bufs == null || bufs.length == 0 ) - throw new Exception( "buffer is empty" ); - - this.bufs = bufs; - for ( ByteBuffer b : bufs ) - b.flip(); - } - - public int read() { - do { - if ( bufs[currentBuf].hasRemaining() ) - return bufs[currentBuf].get(); - currentBuf++; - } - while ( currentBuf < bufs.length ); - - currentBuf--; - return -1; - } - - public int read( byte[] buf ) { - int len = buf.length; - int bufPos = 0; - do { - if ( bufs[currentBuf].hasRemaining() ) { - int n = Math.min( bufs[currentBuf].remaining(), len-bufPos ); - bufs[currentBuf].get( buf, bufPos, n ); - bufPos += n; - } - currentBuf++; - } - while ( currentBuf < bufs.length && bufPos < len ); - - currentBuf--; - - if ( bufPos > 0 || ( bufPos == 0 && len == 0 ) ) - return bufPos; - else - return -1; - } - - public String readLine() throws IOException { - byte[] b = new byte[1]; - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - boolean eol = false; - - while ( read( b, 0, 1 ) != -1 ) { - if ( b[0] == 13 ) { - eol = true; - } - else { - if ( eol ) { - if ( b[0] == 10 ) - break; - eol = false; - } - } - - // cast byte into char array - bos.write( b, 0, 1 ); - } - - if ( bos == null || bos.size() <= 0 ) { - throw new IOException( "++++ Stream appears to be dead, so closing it down" ); - } - - // else return the string - return bos.toString().trim(); - } - - public void clearEOL() throws IOException { - byte[] b = new byte[1]; - boolean eol = false; - while ( read( b, 0, 1 ) != -1 ) { - - // only stop when we see - // \r (13) followed by \n (10) - if ( b[0] == 13 ) { - eol = true; - continue; - } - - if ( eol ) { - if ( b[0] == 10 ) - break; - eol = false; - } - } - } - - public String toString() { - StringBuilder sb = new StringBuilder( "ByteBufArrayIS: " ); - sb.append( bufs.length ).append( " bufs of sizes: \n" ); - - for ( int i=0; i < bufs.length; i++ ) { - sb.append( " " ) - .append (i ).append( ": " ).append( bufs[i] ).append( "\n" ); - } - return sb.toString(); - } -} diff --git a/src/com/meetup/memcached/ContextObjectInputStream.java b/src/com/meetup/memcached/ContextObjectInputStream.java deleted file mode 100644 index daec86c..0000000 --- a/src/com/meetup/memcached/ContextObjectInputStream.java +++ /dev/null @@ -1,44 +0,0 @@ -/** - * 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. - * - * Adds the ability for the MemCached client to be initialized - * with a custom class loader. This will allow for the - * deserialization of classes that are not visible to the system - * class loader. - * - * @author Vin Chawla - */ -package com.meetup.memcached; - -import java.util.*; -import java.util.zip.*; -import java.io.*; - -public class ContextObjectInputStream extends ObjectInputStream { - - ClassLoader mLoader; - - public ContextObjectInputStream( InputStream in, ClassLoader loader ) throws IOException, SecurityException { - super( in ); - mLoader = loader; - } - - protected Class resolveClass( ObjectStreamClass v ) throws IOException, ClassNotFoundException { - if ( mLoader == null ) - return super.resolveClass( v ); - else - return Class.forName( v.getName(), true, mLoader ); - } -} diff --git a/src/com/meetup/memcached/ErrorHandler.java b/src/com/meetup/memcached/ErrorHandler.java deleted file mode 100644 index 0e84290..0000000 --- a/src/com/meetup/memcached/ErrorHandler.java +++ /dev/null @@ -1,72 +0,0 @@ -/** - * 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. - * - * This is an interface implemented by classes that want to receive callbacks - * in the event of an error in {@link MemcachedClient}. The implementor can do - * things like flush caches or perform additioonal logging. - * - * @author Dan Zivkovic - */ -package com.meetup.memcached; - -public interface ErrorHandler { - - /** - * Called for errors thrown during initialization. - */ - public void handleErrorOnInit( final MemcachedClient client , - final Throwable error ); - - /** - * Called for errors thrown during {@link MemcachedClient#get(String)} and related methods. - */ - public void handleErrorOnGet( final MemcachedClient client , - final Throwable error , - final String cacheKey ); - - /** - * Called for errors thrown during {@link MemcachedClient#getMulti(String)} and related methods. - */ - public void handleErrorOnGet( final MemcachedClient client , - final Throwable error , - final String[] cacheKeys ); - - /** - * Called for errors thrown during {@link MemcachedClient#set(String,Object)} and related methods. - */ - public void handleErrorOnSet( final MemcachedClient client , - final Throwable error , - final String cacheKey ); - - /** - * Called for errors thrown during {@link MemcachedClient#delete(String)} and related methods. - */ - public void handleErrorOnDelete( final MemcachedClient client , - final Throwable error , - final String cacheKey ); - - /** - * Called for errors thrown during {@link MemcachedClient#flushAll()} and related methods. - */ - public void handleErrorOnFlush( final MemcachedClient client , - final Throwable error ); - - /** - * Called for errors thrown during {@link MemcachedClient#stats()} and related methods. - */ - public void handleErrorOnStats( final MemcachedClient client , - final Throwable error ); - -} // interface diff --git a/src/com/meetup/memcached/LineInputStream.java b/src/com/meetup/memcached/LineInputStream.java deleted file mode 100644 index 3fee794..0000000 --- a/src/com/meetup/memcached/LineInputStream.java +++ /dev/null @@ -1,43 +0,0 @@ -/** - * 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 - */ -package com.meetup.memcached; - -import java.io.IOException; - -public interface LineInputStream { - - /** - * Read everything up to the next end-of-line. Does - * not include the end of line, though it is consumed - * from the input. - * @return All next up to the next end of line. - */ - public String readLine() throws IOException; - - /** - * Read everything up to and including the end of line. - */ - public void clearEOL() throws IOException; - - /** - * Read some bytes. - * @param buf The buffer into which read. - * @return The number of bytes actually read, or -1 if none could be read. - */ - public int read( byte[] buf ) throws IOException; -} diff --git a/src/com/meetup/memcached/Logger.java b/src/com/meetup/memcached/Logger.java deleted file mode 100644 index f5300fd..0000000 --- a/src/com/meetup/memcached/Logger.java +++ /dev/null @@ -1,202 +0,0 @@ -/** - * 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 - */ -package com.meetup.memcached; - -import java.util.*; - -/** - * This is a generic logger class for use in logging. - * - * This can easily be swapped out for any other logging package in the main code. - * For now, this is just a quick and dirty logger which will allow you to specify - * log levels, but only wraps system.out.println. - * - * @author Greg Whalin - * @version 1.5 - */ -public class Logger { - - public static final int LEVEL_DEBUG = 0; - public static final int LEVEL_INFO = 1; - public static final int LEVEL_WARN = 2; - public static final int LEVEL_ERROR = 3; - public static final int LEVEL_FATAL = 4; - - private static Map loggers = - new HashMap(); - - private String name; - private int level; - private boolean initialized = false; - - public void setLevel( int level ) { this.level = level; } - public int getLevel() { return this.level; } - - protected Logger( String name, int level ) { - this.name = name; - this.level = level; - this.initialized = true; - } - - protected Logger( String name ) { - this.name = name; - this.level = LEVEL_INFO; - this.initialized = true; - } - - /** - * Gets a Logger obj for given name and level. - * - * @param name - * @param level - * @return - */ - public static synchronized Logger getLogger( String name, int level ) { - Logger log = getLogger( name ); - if ( log.getLevel() != level ) - log.setLevel( level ); - - return log; - } - - /** - * Gets a Logger obj for given name - * and sets default level. - * - * @param name - * @return - */ - public static synchronized Logger getLogger( String name ) { - - Logger log = null; - if ( loggers.containsKey( name ) ) { - log = loggers.get( name ); - } - else { - log = new Logger( name ); - loggers.put( name, log ); - } - - return log; - } - - /** - * logs mesg to std out and prints stack trace if exception passed in - * - * @param mesg - * @param ex - */ - private void log( String mesg, Throwable ex ) { - System.out.println( name + " " + new Date() + " - " + mesg ); - if ( ex != null ) - ex.printStackTrace( System.out ); - } - - /** - * logs a debug mesg - * - * @param mesg - * @param ex - */ - public void debug( String mesg, Throwable ex ) { - if ( this.level > LEVEL_DEBUG ) - return; - - log( mesg, ex ); - } - - public void debug( String mesg ) { - debug( mesg, null ); - } - - public boolean isDebugEnabled() { - return this.level <= LEVEL_DEBUG; - } - - /** - * logs info mesg - * - * @param mesg - * @param ex - */ - public void info( String mesg, Throwable ex ) { - if ( this.level > LEVEL_INFO ) - return; - - log( mesg, ex ); - } - - public void info( String mesg ) { - info( mesg, null ); - } - - public boolean isInfoEnabled() { - return this.level <= LEVEL_INFO; - } - - /** - * logs warn mesg - * - * @param mesg - * @param ex - */ - public void warn( String mesg, Throwable ex ) { - if ( this.level > LEVEL_WARN ) - return; - - log( mesg, ex ); - } - - public void warn( String mesg ) { - warn( mesg, null ); - } - - /** - * logs error mesg - * - * @param mesg - * @param ex - */ - public void error( String mesg, Throwable ex ) { - if ( this.level > LEVEL_ERROR ) - return; - - log( mesg, ex ); - } - - public void error( String mesg ) { - error( mesg, null ); - } - - /** - * logs fatal mesg - * - * @param mesg - * @param ex - */ - public void fatal( String mesg, Throwable ex ) { - if ( this.level > LEVEL_FATAL ) - return; - - log( mesg, ex ); - } - - public void fatal( String mesg ) { - fatal( mesg, null ); - } -} diff --git a/src/com/meetup/memcached/MemcachedClient.java b/src/com/meetup/memcached/MemcachedClient.java deleted file mode 100644 index c17c1fa..0000000 --- a/src/com/meetup/memcached/MemcachedClient.java +++ /dev/null @@ -1,2316 +0,0 @@ -/** - * 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 - */ -package com.meetup.memcached; - -import java.util.*; -import java.util.zip.*; -import java.nio.*; -import java.net.InetAddress; -import java.nio.charset.*; -import java.nio.channels.*; -import java.nio.channels.spi.*; -import java.io.*; -import java.net.URLEncoder; - -import org.apache.log4j.Logger; - -/** - * This is a Memcached client for the Java platform available from - * http://www.danga.com/memcached/. - *
- * Supports setting, adding, replacing, deleting compressed/uncompressed and
- * serialized (can be stored as string if object is native class) objects to memcached.
- *
- * Now pulls SockIO objects from SockIOPool, which is a connection pool. The server failover
- * has also been moved into the SockIOPool class.
- * This pool needs to be initialized prior to the client working. See javadocs from SockIOPool.
- *
- * Some examples of use follow.
- *

To create cache client object and set params:

- *
 
- *	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);
- * 
- *

To store an object:

- *
- *	MemcachedClient mc = new MemcachedClient();
- *	String key   = "cacheKey1";	
- *	Object value = SomeClass.getObject();	
- *	mc.set(key, value);
- * 
- *

To store an object using a custom server hashCode:

- *
- *	MemcachedClient mc = new MemcachedClient();
- *	String key   = "cacheKey1";	
- *	Object value = SomeClass.getObject();	
- *	Integer hash = new Integer(45);	
- *	mc.set(key, value, hash);
- * 
- * The set method shown above will always set the object in the cache.
- * The add and replace methods do the same, but with a slight difference.
- *
    - *
  • add -- will store the object only if the server does not have an entry for this key
  • - *
  • replace -- will store the object only if the server already has an entry for this key
  • - *
- *

To delete a cache entry:

- *
- *	MemcachedClient mc = new MemcachedClient();
- *	String key   = "cacheKey1";	
- *	mc.delete(key);
- * 
- *

To delete a cache entry using a custom hash code:

- *
- *	MemcachedClient mc = new MemcachedClient();
- *	String key   = "cacheKey1";	
- *	Integer hash = new Integer(45);	
- *	mc.delete(key, hashCode);
- * 
- *

To store a counter and then increment or decrement that counter:

- *
- *	MemcachedClient mc = new MemcachedClient();
- *	String key   = "counterKey";	
- *	mc.storeCounter(key, new Integer(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));	
- * 
- *

To store a counter and then increment or decrement that counter with custom hash:

- *
- *	MemcachedClient mc = new MemcachedClient();
- *	String key   = "counterKey";	
- *	Integer hash = new Integer(45);	
- *	mc.storeCounter(key, new Integer(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));	
- * 
- *

To retrieve an object from the cache:

- *
- *	MemcachedClient mc = new MemcachedClient();
- *	String key   = "key";	
- *	Object value = mc.get(key);	
- * 
- *

To retrieve an object from the cache with custom hash:

- *
- *	MemcachedClient mc = new MemcachedClient();
- *	String key   = "key";	
- *	Integer hash = new Integer(45);	
- *	Object value = mc.get(key, hash);	
- * 
- *

To retrieve an multiple objects from the cache

- *
- *	MemcachedClient mc = new MemcachedClient();
- *	String[] keys      = { "key", "key1", "key2" };
- *	Map<Object> values = mc.getMulti(keys);
- * 
- *

To retrieve an multiple objects from the cache with custom hashing

- *
- *	MemcachedClient mc = new MemcachedClient();
- *	String[] keys      = { "key", "key1", "key2" };
- *	Integer[] hashes   = { new Integer(45), new Integer(32), new Integer(44) };
- *	Map<Object> values = mc.getMulti(keys, hashes);
- * 
- *

To flush all items in server(s)

- *
- *	MemcachedClient mc = new MemcachedClient();
- *	mc.flushAll();
- * 
- *

To get stats from server(s)

- *
- *	MemcachedClient mc = new MemcachedClient();
- *	Map stats = mc.stats();
- * 
- * - * @author greg whalin - * @author Richard 'toast' Russo - * @author Kevin Burton - * @author Robert Watts - * @author Vin Chawla - * @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 = "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
- * longer than the threshold length set with setCompressThreshold(int)
- *
- * The default is that compression is enabled.
- *
- * Even if compression is disabled, compressed data will be automatically
- * decompressed. - * - * @param compressEnable true to enable compression, false 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 true, 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 true, 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.
- * Keep in mind {@link #add(String, Object) add} and {@link #replace(String, Object) replace}
- * will fail when used with the same key will fail, until the server reaches the
- * specified time. However, {@link #set(String, Object) set} will succeed,
- * 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 true, 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
- * deleted, the specified value will not be stored.
- * The server will automatically delete the value when the expiration time has been reached.
- *
- * If compression is enabled, and the data is longer than the compression threshold
- * the data will be stored in compressed form.
- *
- * 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.error( "trying to store a null value to cache" ); - 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(); - gos.close(); - - // 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, new Long( 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, new Long( 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, new Long( 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
- * underflow. In the event of underflow, the result will be zero. Because
- * Java lacks unsigned types, the value is returned as a 64-bit integer.
- * The server will only decrement a value if it already exists;
- * 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
- * be decompressed or serialized, as appropriate. (Inclusive or)
- *
- * Non-serialized data will be returned as a string, so explicit conversion to
- * numeric types will be necessary, if desired
- * - * @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
- * be decompressed or serialized, as appropriate. (Inclusive or)
- *
- * Non-serialized data will be returned as a string, so explicit conversion to
- * numeric types will be necessary, if desired
- * - * @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
- * be decompressed or serialized, as appropriate. (Inclusive or)
- *
- * Non-serialized data will be returned as a string, so explicit conversion to
- * numeric types will be necessary, if desired
- * - * @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 + " -- " + e.getMessage() ); - 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 ( Exception e ) { - if ( errorHandler != null ) - errorHandler.handleErrorOnGet( this, e, key ); - - o = null; - log.error( "++++ Exception thrown while trying to deserialize for key: " + key + " -- " + e.getMessage() ); - } - } - } - 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 + " -- " + e.getMessage() ); - - 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
- * is more efficient.
- * - * @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
- * is more efficient.
- * - * @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
- * is more efficient.
- * - * @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 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
- * is more efficient.
- * - * @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 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
- * is more efficient.
- * - * @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 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
- * is more efficient.
- * - * @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 getMulti( String[] keys, Integer[] hashCodes, boolean asString ) { - - if ( keys == null || keys.length == 0 ) { - log.error( "missing keys for getMulti()" ); - return null; - } - - Map cmdMap = - new HashMap(); - - 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 ret = - new HashMap( 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
- * 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 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 + " -- " + e.getMessage() ); - 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.getMessage() ); - 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 ( InvalidClassException e ) { - /* Errors de-serializing are to be expected in the case of a - * long running server that spans client restarts with updated - * classes. - */ - // if we have an errorHandler, use its hook - if ( errorHandler != null ) - errorHandler.handleErrorOnGet( this, e, key ); - - o = null; - log.error( "++++ InvalidClassException thrown while trying to deserialize for key: " + key + " -- " + e.getMessage() ); - } - catch ( ClassNotFoundException e ) { - - // if we have an errorHandler, use its hook - if ( errorHandler != null ) - errorHandler.handleErrorOnGet( this, e, key ); - - o = null; - log.error( "++++ ClassNotFoundException thrown while trying to deserialize for key: " + key + " -- " + e.getMessage() ); - } - } - - // store the object into the cache - if ( o != null ) - 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, "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 statsMaps = - new HashMap(); - - 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 stats = new HashMap(); - - // 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 incoming = new ArrayList(); - 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 sockKeys, String[] keys, Map 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 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 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 { - if ( selector != null ) - 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; - } - } - } - } -} diff --git a/src/com/meetup/memcached/NativeHandler.java b/src/com/meetup/memcached/NativeHandler.java deleted file mode 100644 index 7f9a87e..0000000 --- a/src/com/meetup/memcached/NativeHandler.java +++ /dev/null @@ -1,443 +0,0 @@ -/** - * 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 - */ -package com.meetup.memcached; - -import java.util.Date; -import org.apache.log4j.Logger; - -/** - * Handle encoding standard Java types directly which can result in significant - * memory savings: - * - * Currently the Memcached driver for Java supports the setSerialize() option. - * This can increase performance in some situations but has a few issues: - * - * Code that performs class casting will throw ClassCastExceptions when - * setSerialize is enabled. For example: - * - * mc.set( "foo", new Integer( 1 ) ); Integer output = (Integer)mc.get("foo"); - * - * Will work just file when setSerialize is true but when its false will just throw - * a ClassCastException. - * - * Also internally it doesn't support Boolean and since toString is called wastes a - * lot of memory and causes additional performance issue. For example an Integer - * can take anywhere from 1 byte to 10 bytes. - * - * Due to the way the memcached slab allocator works it seems like a LOT of wasted - * memory to store primitive types as serialized objects (from a performance and - * memory perspective). In our applications we have millions of small objects and - * wasted memory would become a big problem. - * - * For example a Serialized Boolean takes 47 bytes which means it will fit into the - * 64byte LRU. Using 1 byte means it will fit into the 8 byte LRU thus saving 8x - * the memory. This also saves the CPU performance since we don't have to - * serialize bytes back and forth and we can compute the byte[] value directly. - * - * One problem would be when the user calls get() because doing so would require - * the app to know the type of the object stored as a bytearray inside memcached - * (since the user will probably cast). - * - * If we assume the basic types are interned we could use the first byte as the - * type with the remaining bytes as the value. Then on get() we could read the - * first byte to determine the type and then construct the correct object for it. - * This would prevent the ClassCastException I talked about above. - * - * We could remove the setSerialize() option and just assume that standard VM types - * are always internd in this manner. - * - * mc.set( "foo", new Boolean.TRUE ); Boolean b = (Boolean)mc.get( "foo" ); - * - * And the type casts would work because internally we would create a new Boolean - * to return back to the client. - * - * This would reduce memory footprint and allow for a virtual implementation of the - * Externalizable interface which is much faster than Serialzation. - * - * Currently the memory improvements would be: - * - * java.lang.Boolean - 8x performance improvement (now just two bytes) - * java.lang.Integer - 16x performance improvement (now just 5 bytes) - * - * Most of the other primitive types would benefit from this optimization. - * java.lang.Character being another obvious example. - * - * I know it seems like I'm being really picky here but for our application I'd - * save 1G of memory right off the bat. We'd go down from 1.152G of memory used - * down to 144M of memory used which is much better IMO. - * - * http://java.sun.com/docs/books/tutorial/native1.1/integrating/types.html - * - * @author Kevin A. Burton - * @author Greg Whalin - */ -public class NativeHandler { - - // logger - private static Logger log = - Logger.getLogger( NativeHandler.class.getName() ); - - /** - * Detemine of object can be natively serialized by this class. - * - * @param value Object to test. - * @return true/false - */ - public static boolean isHandled( Object value ) { - - return ( - value instanceof Byte || - value instanceof Boolean || - value instanceof Integer || - value instanceof Long || - value instanceof Character || - value instanceof String || - value instanceof StringBuffer || - value instanceof Float || - value instanceof Short || - value instanceof Double || - value instanceof Date || - value instanceof StringBuilder || - value instanceof byte[] - ) - ? true - : false; - } - - /** - * Returns the flag for marking the type of the byte array. - * - * @param value Object we are storing. - * @return int marker - */ - public static int getMarkerFlag( Object value ) { - - if ( value instanceof Byte ) - return MemcachedClient.MARKER_BYTE; - - if ( value instanceof Boolean ) - return MemcachedClient.MARKER_BOOLEAN; - - if ( value instanceof Integer ) - return MemcachedClient.MARKER_INTEGER; - - if ( value instanceof Long ) - return MemcachedClient.MARKER_LONG; - - if ( value instanceof Character ) - return MemcachedClient.MARKER_CHARACTER; - - if ( value instanceof String ) - return MemcachedClient.MARKER_STRING; - - if ( value instanceof StringBuffer ) - return MemcachedClient.MARKER_STRINGBUFFER; - - if ( value instanceof Float ) - return MemcachedClient.MARKER_FLOAT; - - if ( value instanceof Short ) - return MemcachedClient.MARKER_SHORT; - - if ( value instanceof Double ) - return MemcachedClient.MARKER_DOUBLE; - - if ( value instanceof Date ) - return MemcachedClient.MARKER_DATE; - - if ( value instanceof StringBuilder ) - return MemcachedClient.MARKER_STRINGBUILDER; - - if ( value instanceof byte[] ) - return MemcachedClient.MARKER_BYTEARR; - - return -1; - } - - /** - * Encodes supported types - * - * @param value Object to encode. - * @return byte array - * - * @throws Exception If fail to encode. - */ - public static byte[] encode( Object value ) throws Exception { - - if ( value instanceof Byte ) - return encode( (Byte)value ); - - if ( value instanceof Boolean ) - return encode( (Boolean)value ); - - if ( value instanceof Integer ) - return encode( ((Integer)value).intValue() ); - - if ( value instanceof Long ) - return encode( ((Long)value).longValue() ); - - if ( value instanceof Character ) - return encode( (Character)value ); - - if ( value instanceof String ) - return encode( (String)value ); - - if ( value instanceof StringBuffer ) - return encode( (StringBuffer)value ); - - if ( value instanceof Float ) - return encode( ((Float)value).floatValue() ); - - if ( value instanceof Short ) - return encode( (Short)value ); - - if ( value instanceof Double ) - return encode( ((Double)value).doubleValue() ); - - if ( value instanceof Date ) - return encode( (Date)value); - - if ( value instanceof StringBuilder ) - return encode( (StringBuilder)value ); - - if ( value instanceof byte[] ) - return encode( (byte[])value ); - - return null; - } - - protected static byte[] encode( Byte value ) { - byte[] b = new byte[1]; - b[0] = value.byteValue(); - return b; - } - - protected static byte[] encode( Boolean value ) { - byte[] b = new byte[1]; - - if ( value.booleanValue() ) - b[0] = 1; - else - b[0] = 0; - - return b; - } - - protected static byte[] encode( int value ) { - return getBytes( value ); - } - - protected static byte[] encode( long value ) throws Exception { - return getBytes( value ); - } - - protected static byte[] encode( Date value ) { - return getBytes( value.getTime() ); - } - - protected static byte[] encode( Character value ) { - return encode( value.charValue() ); - } - - protected static byte[] encode( String value ) throws Exception { - return value.getBytes( "UTF-8" ); - } - - protected static byte[] encode( StringBuffer value ) throws Exception { - return encode( value.toString() ); - } - - protected static byte[] encode( float value ) throws Exception { - return encode( (int)Float.floatToIntBits( value ) ); - } - - protected static byte[] encode( Short value ) throws Exception { - return encode( (int)value.shortValue() ); - } - - protected static byte[] encode( double value ) throws Exception { - return encode( (long)Double.doubleToLongBits( value ) ); - } - - protected static byte[] encode( StringBuilder value ) throws Exception { - return encode( value.toString() ); - } - - protected static byte[] encode( byte[] value ) { - return value; - } - - protected static byte[] getBytes( long value ) { - byte[] b = new byte[8]; - b[0] = (byte)((value >> 56) & 0xFF); - b[1] = (byte)((value >> 48) & 0xFF); - b[2] = (byte)((value >> 40) & 0xFF); - b[3] = (byte)((value >> 32) & 0xFF); - b[4] = (byte)((value >> 24) & 0xFF); - b[5] = (byte)((value >> 16) & 0xFF); - b[6] = (byte)((value >> 8) & 0xFF); - b[7] = (byte)((value >> 0) & 0xFF); - return b; - } - - protected static byte[] getBytes( int value ) { - byte[] b = new byte[4]; - b[0] = (byte)((value >> 24) & 0xFF); - b[1] = (byte)((value >> 16) & 0xFF); - b[2] = (byte)((value >> 8) & 0xFF); - b[3] = (byte)((value >> 0) & 0xFF); - return b; - } - - /** - * Decodes byte array using memcache flag to determine type. - * - * @param b - * @param marker - * @return - * @throws Exception - */ - public static Object decode( byte[] b, int flag ) throws Exception { - - if ( b.length < 1 ) - return null; - - - if ( ( flag & MemcachedClient.MARKER_BYTE ) == MemcachedClient.MARKER_BYTE ) - return decodeByte( b ); - - if ( ( flag & MemcachedClient.MARKER_BOOLEAN ) == MemcachedClient.MARKER_BOOLEAN ) - return decodeBoolean( b ); - - if ( ( flag & MemcachedClient.MARKER_INTEGER ) == MemcachedClient.MARKER_INTEGER ) - return decodeInteger( b ); - - if ( ( flag & MemcachedClient.MARKER_LONG ) == MemcachedClient.MARKER_LONG ) - return decodeLong( b ); - - if ( ( flag & MemcachedClient.MARKER_CHARACTER ) == MemcachedClient.MARKER_CHARACTER ) - return decodeCharacter( b ); - - if ( ( flag & MemcachedClient.MARKER_STRING ) == MemcachedClient.MARKER_STRING ) - return decodeString( b ); - - if ( ( flag & MemcachedClient.MARKER_STRINGBUFFER ) == MemcachedClient.MARKER_STRINGBUFFER ) - return decodeStringBuffer( b ); - - if ( ( flag & MemcachedClient.MARKER_FLOAT ) == MemcachedClient.MARKER_FLOAT ) - return decodeFloat( b ); - - if ( ( flag & MemcachedClient.MARKER_SHORT ) == MemcachedClient.MARKER_SHORT ) - return decodeShort( b ); - - if ( ( flag & MemcachedClient.MARKER_DOUBLE ) == MemcachedClient.MARKER_DOUBLE ) - return decodeDouble( b ); - - if ( ( flag & MemcachedClient.MARKER_DATE ) == MemcachedClient.MARKER_DATE ) - return decodeDate( b ); - - if ( ( flag & MemcachedClient.MARKER_STRINGBUILDER ) == MemcachedClient.MARKER_STRINGBUILDER ) - return decodeStringBuilder( b ); - - if ( ( flag & MemcachedClient.MARKER_BYTEARR ) == MemcachedClient.MARKER_BYTEARR ) - return decodeByteArr( b ); - - return null; - } - - // decode methods - protected static Byte decodeByte( byte[] b ) { - return new Byte( b[0] ); - } - - protected static Boolean decodeBoolean( byte[] b ) { - boolean value = b[0] == 1; - return ( value ) ? Boolean.TRUE : Boolean.FALSE; - } - - protected static Integer decodeInteger( byte[] b ) { - return new Integer( toInt( b ) ); - } - - protected static Long decodeLong( byte[] b ) throws Exception { - return new Long( toLong( b ) ); - } - - protected static Character decodeCharacter( byte[] b ) { - return new Character( (char)decodeInteger( b ).intValue() ); - } - - protected static String decodeString( byte[] b ) throws Exception { - return new String( b, "UTF-8" ); - } - - protected static StringBuffer decodeStringBuffer( byte[] b ) throws Exception { - return new StringBuffer( decodeString( b ) ); - } - - protected static Float decodeFloat( byte[] b ) throws Exception { - Integer l = decodeInteger( b ); - return new Float( Float.intBitsToFloat( l.intValue() ) ); - } - - protected static Short decodeShort( byte[] b ) throws Exception { - return new Short( (short)decodeInteger( b ).intValue() ); - } - - protected static Double decodeDouble( byte[] b ) throws Exception { - Long l = decodeLong( b ); - return new Double( Double.longBitsToDouble( l.longValue() ) ); - } - - protected static Date decodeDate( byte[] b ) { - return new Date( toLong( b ) ); - } - - protected static StringBuilder decodeStringBuilder( byte[] b ) throws Exception { - return new StringBuilder( decodeString( b ) ); - } - - protected static byte[] decodeByteArr( byte[] b ) { - return b; - } - - /** - * This works by taking each of the bit patterns and converting them to - * ints taking into account 2s complement and then adding them.. - * - * @param b - * @return - */ - protected static int toInt( byte[] b ) { - return (((((int) b[3]) & 0xFF) << 32) + - ((((int) b[2]) & 0xFF) << 40) + - ((((int) b[1]) & 0xFF) << 48) + - ((((int) b[0]) & 0xFF) << 56)); - } - - protected static long toLong( byte[] b ) { - return ((((long) b[7]) & 0xFF) + - ((((long) b[6]) & 0xFF) << 8) + - ((((long) b[5]) & 0xFF) << 16) + - ((((long) b[4]) & 0xFF) << 24) + - ((((long) b[3]) & 0xFF) << 32) + - ((((long) b[2]) & 0xFF) << 40) + - ((((long) b[1]) & 0xFF) << 48) + - ((((long) b[0]) & 0xFF) << 56)); - } -} diff --git a/src/com/meetup/memcached/NestedIOException.java b/src/com/meetup/memcached/NestedIOException.java deleted file mode 100644 index 264605e..0000000 --- a/src/com/meetup/memcached/NestedIOException.java +++ /dev/null @@ -1,44 +0,0 @@ -/** - * 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 Kevin A. Burton - */ -package com.meetup.memcached; - -import java.io.*; - -/** - * Bridge class to provide nested Exceptions with IOException which has - * constructors that don't take Throwables. - * - * @author Kevin Burton - * @version 1.2 - */ -public class NestedIOException extends IOException { - - /** - * Create a new NestedIOException instance. - * @param cause object of type throwable - */ - public NestedIOException( Throwable cause ) { - super( cause.getMessage() ); - super.initCause( cause ); - } - - public NestedIOException( String message, Throwable cause ) { - super( message ); - initCause( cause ); - } -} diff --git a/src/com/meetup/memcached/SockIOPool.java b/src/com/meetup/memcached/SockIOPool.java deleted file mode 100644 index c649b9a..0000000 --- a/src/com/meetup/memcached/SockIOPool.java +++ /dev/null @@ -1,1903 +0,0 @@ -/** - * 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 - */ -package com.meetup.memcached; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -// java.util -import java.util.Map; -import java.util.List; -import java.util.Set; -import java.util.Iterator; -import java.util.ArrayList; -import java.util.IdentityHashMap; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Date; -import java.util.Arrays; -import java.util.SortedMap; -import java.util.TreeMap; - -import java.util.zip.*; -import java.net.*; -import java.io.*; -import java.nio.*; -import java.nio.channels.*; -import java.util.concurrent.locks.ReentrantLock; -import org.apache.log4j.Logger; - -/** - * This class is a connection pool for maintaning a pool of persistent connections
- * to memcached servers. - * - * The pool must be initialized prior to use. This should typically be early on
- * in the lifecycle of the JVM instance.
- *
- *

An example of initializing using defaults:

- *
- *
- *	static {
- *		String[] serverlist = { "cache0.server.com:12345", "cache1.server.com:12345" };
- *
- *		SockIOPool pool = SockIOPool.getInstance();
- *		pool.setServers(serverlist);
- *		pool.initialize();	
- *	}
- * 
- *

An example of initializing using defaults and providing weights for servers:

- *
- *	static {
- *		String[] serverlist = { "cache0.server.com:12345", "cache1.server.com:12345" };
- *		Integer[] weights   = { new Integer(5), new Integer(2) };
- *		
- *		SockIOPool pool = SockIOPool.getInstance();
- *		pool.setServers(serverlist);
- *		pool.setWeights(weights);	
- *		pool.initialize();	
- *	}
- *  
- *

An example of initializing overriding defaults:

- *
- *	static {
- *		String[] serverlist     = { "cache0.server.com:12345", "cache1.server.com:12345" };
- *		Integer[] weights       = { new Integer(5), new Integer(2) };	
- *		int initialConnections  = 10;
- *		int minSpareConnections = 5;
- *		int maxSpareConnections = 50;	
- *		long maxIdleTime        = 1000 * 60 * 30;	// 30 minutes
- *		long maxBusyTime        = 1000 * 60 * 5;	// 5 minutes
- *		long maintThreadSleep   = 1000 * 5;			// 5 seconds
- *		int	socketTimeOut       = 1000 * 3;			// 3 seconds to block on reads
- *		int	socketConnectTO     = 1000 * 3;			// 3 seconds to block on initial connections.  If 0, then will use blocking connect (default)
- *		boolean failover        = false;			// turn off auto-failover in event of server down	
- *		boolean nagleAlg        = false;			// turn off Nagle's algorithm on all sockets in pool	
- *		boolean aliveCheck      = false;			// disable health check of socket on checkout
- *
- *		SockIOPool pool = SockIOPool.getInstance();
- *		pool.setServers( serverlist );
- *		pool.setWeights( weights );	
- *		pool.setInitConn( initialConnections );
- *		pool.setMinConn( minSpareConnections );
- *		pool.setMaxConn( maxSpareConnections );
- *		pool.setMaxIdle( maxIdleTime );
- *		pool.setMaxBusyTime( maxBusyTime );
- *		pool.setMaintSleep( maintThreadSleep );
- *		pool.setSocketTO( socketTimeOut );
- *		pool.setNagle( nagleAlg );	
- *		pool.setHashingAlg( SockIOPool.NEW_COMPAT_HASH );
- *		pool.setAliveCheck( true );
- *		pool.initialize();	
- *	}
- *  
- * The easiest manner in which to initialize the pool is to set the servers and rely on defaults as in the first example.
- * After pool is initialized, a client will request a SockIO object by calling getSock with the cache key
- * The client must always close the SockIO object when finished, which will return the connection back to the pool.
- *

An example of retrieving a SockIO object:

- *
- *		SockIOPool.SockIO sock = SockIOPool.getInstance().getSock( key );
- *		try {
- *			sock.write( "version\r\n" );	
- *			sock.flush();	
- *			System.out.println( "Version: " + sock.readLine() );	
- *		}
- *		catch (IOException ioe) { System.out.println( "io exception thrown" ) };	
- *
- *		sock.close();	
- * 
- * - * @author greg whalin - * @version 1.5 - */ -public class SockIOPool { - - // logger - private static Logger log = - Logger.getLogger( SockIOPool.class.getName() ); - - // store instances of pools - private static Map pools = - new HashMap(); - - // avoid recurring construction - private static ThreadLocal MD5 = new ThreadLocal() { - @Override - protected MessageDigest initialValue() { - try { - return MessageDigest.getInstance( "MD5" ); - } - catch ( NoSuchAlgorithmException e ) { - log.error( "++++ no md5 algorithm found" ); - throw new IllegalStateException( "++++ no md5 algorythm found"); - } - } - }; - - // Constants - private static final Integer ZERO = new Integer( 0 ); - public static final int NATIVE_HASH = 0; // native String.hashCode(); - public static final int OLD_COMPAT_HASH = 1; // original compatibility hashing algorithm (works with other clients) - public static final int NEW_COMPAT_HASH = 2; // new CRC32 based compatibility hashing algorithm (works with other clients) - public static final int CONSISTENT_HASH = 3; // MD5 Based -- Stops thrashing when a server added or removed - - public static final long MAX_RETRY_DELAY = 10 * 60 * 1000; // max of 10 minute delay for fall off - - // Pool data - private MaintThread maintThread; - private boolean initialized = false; - private int maxCreate = 1; // this will be initialized by pool when the pool is initialized - - // initial, min and max pool sizes - private int poolMultiplier = 3; - private int initConn = 10; - private int minConn = 5; - private int maxConn = 100; - private long maxIdle = 1000 * 60 * 5; // max idle time for avail sockets - private long maxBusyTime = 1000 * 30; // max idle time for avail sockets - private long maintSleep = 1000 * 30; // maintenance thread sleep time - private int socketTO = 1000 * 3; // default timeout of socket reads - private int socketConnectTO = 1000 * 3; // default timeout of socket connections - private boolean aliveCheck = false; // default to not check each connection for being alive - private boolean failover = true; // default to failover in event of cache server dead - private boolean failback = true; // only used if failover is also set ... controls putting a dead server back into rotation - private boolean nagle = false; // enable/disable Nagle's algorithm - private int hashingAlg = NATIVE_HASH; // default to using the native hash as it is the fastest - - // locks - private final ReentrantLock hostDeadLock = new ReentrantLock(); - - // list of all servers - private String[] servers; - private Integer[] weights; - private Integer totalWeight = 0; - - private List buckets; - private TreeMap consistentBuckets; - - // dead server map - private Map hostDead; - private Map hostDeadDur; - - // map to hold all available sockets - // map to hold busy sockets - // set to hold sockets to close - private Map> availPool; - private Map> busyPool; - private Map deadPool;; - - // empty constructor - protected SockIOPool() { } - - /** - * Factory to create/retrieve new pools given a unique poolName. - * - * @param poolName unique name of the pool - * @return instance of SockIOPool - */ - public static synchronized SockIOPool getInstance( String poolName ) { - if ( pools.containsKey( poolName ) ) - return pools.get( poolName ); - - SockIOPool pool = new SockIOPool(); - pools.put( poolName, pool ); - - return pool; - } - - /** - * Single argument version of factory used for back compat. - * Simply creates a pool named "default". - * - * @return instance of SockIOPool - */ - public static SockIOPool getInstance() { - return getInstance( "default" ); - } - - /** - * Sets the list of all cache servers. - * - * @param servers String array of servers [host:port] - */ - public void setServers( String[] servers ) { this.servers = servers; } - - /** - * Returns the current list of all cache servers. - * - * @return String array of servers [host:port] - */ - public String[] getServers() { return this.servers; } - - /** - * Sets the list of weights to apply to the server list. - * - * This is an int array with each element corresponding to an element
- * in the same position in the server String array. - * - * @param weights Integer array of weights - */ - public void setWeights( Integer[] weights ) { this.weights = weights; } - - /** - * Returns the current list of weights. - * - * @return int array of weights - */ - public Integer[] getWeights() { return this.weights; } - - /** - * Sets the initial number of connections per server in the available pool. - * - * @param initConn int number of connections - */ - public void setInitConn( int initConn ) { this.initConn = initConn; } - - /** - * Returns the current setting for the initial number of connections per server in - * the available pool. - * - * @return number of connections - */ - public int getInitConn() { return this.initConn; } - - /** - * Sets the minimum number of spare connections to maintain in our available pool. - * - * @param minConn number of connections - */ - public void setMinConn( int minConn ) { this.minConn = minConn; } - - /** - * Returns the minimum number of spare connections in available pool. - * - * @return number of connections - */ - public int getMinConn() { return this.minConn; } - - /** - * Sets the maximum number of spare connections allowed in our available pool. - * - * @param maxConn number of connections - */ - public void setMaxConn( int maxConn ) { this.maxConn = maxConn; } - - /** - * Returns the maximum number of spare connections allowed in available pool. - * - * @return number of connections - */ - public int getMaxConn() { return this.maxConn; } - - /** - * Sets the max idle time for threads in the available pool. - * - * @param maxIdle idle time in ms - */ - public void setMaxIdle( long maxIdle ) { this.maxIdle = maxIdle; } - - /** - * Returns the current max idle setting. - * - * @return max idle setting in ms - */ - public long getMaxIdle() { return this.maxIdle; } - - /** - * Sets the max busy time for threads in the busy pool. - * - * @param maxBusyTime idle time in ms - */ - public void setMaxBusyTime( long maxBusyTime ) { this.maxBusyTime = maxBusyTime; } - - /** - * Returns the current max busy setting. - * - * @return max busy setting in ms - */ - public long getMaxBusy() { return this.maxBusyTime; } - - /** - * Set the sleep time between runs of the pool maintenance thread. - * If set to 0, then the maint thread will not be started. - * - * @param maintSleep sleep time in ms - */ - public void setMaintSleep( long maintSleep ) { this.maintSleep = maintSleep; } - - /** - * Returns the current maint thread sleep time. - * - * @return sleep time in ms - */ - public long getMaintSleep() { return this.maintSleep; } - - /** - * Sets the socket timeout for reads. - * - * @param socketTO timeout in ms - */ - public void setSocketTO( int socketTO ) { this.socketTO = socketTO; } - - /** - * Returns the socket timeout for reads. - * - * @return timeout in ms - */ - public int getSocketTO() { return this.socketTO; } - - /** - * Sets the socket timeout for connect. - * - * @param socketConnectTO timeout in ms - */ - public void setSocketConnectTO( int socketConnectTO ) { this.socketConnectTO = socketConnectTO; } - - /** - * Returns the socket timeout for connect. - * - * @return timeout in ms - */ - public int getSocketConnectTO() { return this.socketConnectTO; } - - /** - * Sets the failover flag for the pool. - * - * If this flag is set to true, and a socket fails to connect,
- * the pool will attempt to return a socket from another server
- * if one exists. If set to false, then getting a socket
- * will return null if it fails to connect to the requested server. - * - * @param failover true/false - */ - public void setFailover( boolean failover ) { this.failover = failover; } - - /** - * Returns current state of failover flag. - * - * @return true/false - */ - public boolean getFailover() { return this.failover; } - - /** - * Sets the failback flag for the pool. - * - * If this is true and we have marked a host as dead, - * will try to bring it back. If it is false, we will never - * try to resurrect a dead host. - * - * @param failback true/false - */ - public void setFailback( boolean failback ) { this.failback = failback; } - - /** - * Returns current state of failover flag. - * - * @return true/false - */ - public boolean getFailback() { return this.failback; } - - /** - * Sets the aliveCheck flag for the pool. - * - * When true, this will attempt to talk to the server on - * every connection checkout to make sure the connection is - * still valid. This adds extra network chatter and thus is - * defaulted off. May be useful if you want to ensure you do - * not have any problems talking to the server on a dead connection. - * - * @param aliveCheck true/false - */ - public void setAliveCheck( boolean aliveCheck ) { this.aliveCheck = aliveCheck; } - - - /** - * Returns the current status of the aliveCheck flag. - * - * @return true / false - */ - public boolean getAliveCheck() { return this.aliveCheck; } - - /** - * Sets the Nagle alg flag for the pool. - * - * If false, will turn off Nagle's algorithm on all sockets created. - * - * @param nagle true/false - */ - public void setNagle( boolean nagle ) { this.nagle = nagle; } - - /** - * Returns current status of nagle flag - * - * @return true/false - */ - public boolean getNagle() { return this.nagle; } - - /** - * Sets the hashing algorithm we will use. - * - * The types are as follows. - * - * SockIOPool.NATIVE_HASH (0) - native String.hashCode() - fast (cached) but not compatible with other clients - * SockIOPool.OLD_COMPAT_HASH (1) - original compatibility hashing alg (works with other clients) - * SockIOPool.NEW_COMPAT_HASH (2) - new CRC32 based compatibility hashing algorithm (fast and works with other clients) - * - * @param alg int value representing hashing algorithm - */ - public void setHashingAlg( int alg ) { this.hashingAlg = alg; } - - /** - * Returns current status of customHash flag - * - * @return true/false - */ - public int getHashingAlg() { return this.hashingAlg; } - - /** - * Internal private hashing method. - * - * This is the original hashing algorithm from other clients. - * Found to be slow and have poor distribution. - * - * @param key String to hash - * @return hashCode for this string using our own hashing algorithm - */ - private static long origCompatHashingAlg( String key ) { - long hash = 0; - char[] cArr = key.toCharArray(); - - for ( int i = 0; i < cArr.length; ++i ) { - hash = (hash * 33) + cArr[i]; - } - - return hash; - } - - /** - * Internal private hashing method. - * - * This is the new hashing algorithm from other clients. - * Found to be fast and have very good distribution. - * - * UPDATE: This is dog slow under java - * - * @param key - * @return - */ - private static long newCompatHashingAlg( String key ) { - CRC32 checksum = new CRC32(); - checksum.update( key.getBytes() ); - long crc = checksum.getValue(); - return (crc >> 16) & 0x7fff; - } - - /** - * Internal private hashing method. - * - * MD5 based hash algorithm for use in the consistent - * hashing approach. - * - * @param key - * @return - */ - private static long md5HashingAlg( String key ) { - MessageDigest md5 = MD5.get(); - md5.reset(); - md5.update( key.getBytes() ); - byte[] bKey = md5.digest(); - long res = ((long)(bKey[3]&0xFF) << 24) | ((long)(bKey[2]&0xFF) << 16) | ((long)(bKey[1]&0xFF) << 8) | (long)(bKey[0]&0xFF); - return res; - } - - /** - * Returns a bucket to check for a given key. - * - * @param key String key cache is stored under - * @return int bucket - */ - private long getHash( String key, Integer hashCode ) { - - if ( hashCode != null ) { - if ( hashingAlg == CONSISTENT_HASH ) - return hashCode.longValue() & 0xffffffffL; - else - return hashCode.longValue(); - } - else { - switch ( hashingAlg ) { - case NATIVE_HASH: - return (long)key.hashCode(); - case OLD_COMPAT_HASH: - return origCompatHashingAlg( key ); - case NEW_COMPAT_HASH: - return newCompatHashingAlg( key ); - case CONSISTENT_HASH: - return md5HashingAlg( key ); - default: - // use the native hash as a default - hashingAlg = NATIVE_HASH; - return (long)key.hashCode(); - } - } - } - - private long getBucket( String key, Integer hashCode ) { - long hc = getHash( key, hashCode ); - - if ( this.hashingAlg == CONSISTENT_HASH ) { - return findPointFor( hc ); - } - else { - long bucket = hc % buckets.size(); - if ( bucket < 0 ) bucket *= -1; - return bucket; - } - } - - /** - * Gets the first available key equal or above the given one, if none found, - * returns the first k in the bucket - * @param k key - * @return - */ - private Long findPointFor( Long hv ) { - // this works in java 6, but still want to release support for java5 - //Long k = this.consistentBuckets.ceilingKey( hv ); - //return ( k == null ) ? this.consistentBuckets.firstKey() : k; - - SortedMap tmap = - this.consistentBuckets.tailMap( hv ); - - return ( tmap.isEmpty() ) ? this.consistentBuckets.firstKey() : tmap.firstKey(); - } - - /** - * Initializes the pool. - */ - public void initialize() { - - synchronized( this ) { - - // check to see if already initialized - if ( initialized - && ( buckets != null || consistentBuckets != null ) - && ( availPool != null ) - && ( busyPool != null ) ) { - log.error( "++++ trying to initialize an already initialized pool" ); - return; - } - - // pools - availPool = new HashMap>( servers.length * initConn ); - busyPool = new HashMap>( servers.length * initConn ); - deadPool = new IdentityHashMap(); - - hostDeadDur = new HashMap(); - hostDead = new HashMap(); - maxCreate = (poolMultiplier > minConn) ? minConn : minConn / poolMultiplier; // only create up to maxCreate connections at once - - if ( log.isDebugEnabled() ) { - log.debug( "++++ initializing pool with following settings:" ); - log.debug( "++++ initial size: " + initConn ); - log.debug( "++++ min spare : " + minConn ); - log.debug( "++++ max spare : " + maxConn ); - } - - // if servers is not set, or it empty, then - // throw a runtime exception - if ( servers == null || servers.length <= 0 ) { - log.error( "++++ trying to initialize with no servers" ); - throw new IllegalStateException( "++++ trying to initialize with no servers" ); - } - - // initalize our internal hashing structures - if ( this.hashingAlg == CONSISTENT_HASH ) - populateConsistentBuckets(); - else - populateBuckets(); - - // mark pool as initialized - this.initialized = true; - - // start maint thread - if ( this.maintSleep > 0 ) - this.startMaintThread(); - } - } - - private void populateBuckets() { - if ( log.isDebugEnabled() ) - log.debug( "++++ initializing internal hashing structure for consistent hashing" ); - - // store buckets in tree map - this.buckets = new ArrayList(); - - for ( int i = 0; i < servers.length; i++ ) { - if ( this.weights != null && this.weights.length > i ) { - for ( int k = 0; k < this.weights[i].intValue(); k++ ) { - this.buckets.add( servers[i] ); - if ( log.isDebugEnabled() ) - log.debug( "++++ added " + servers[i] + " to server bucket" ); - } - } - else { - this.buckets.add( servers[i] ); - if ( log.isDebugEnabled() ) - log.debug( "++++ added " + servers[i] + " to server bucket" ); - } - - // create initial connections - if ( log.isDebugEnabled() ) - log.debug( "+++ creating initial connections (" + initConn + ") for host: " + servers[i] ); - - for ( int j = 0; j < initConn; j++ ) { - SockIO socket = createSocket( servers[i] ); - if ( socket == null ) { - log.error( "++++ failed to create connection to: " + servers[i] + " -- only " + j + " created." ); - break; - } - - addSocketToPool( availPool, servers[i], socket ); - if ( log.isDebugEnabled() ) - log.debug( "++++ created and added socket: " + socket.toString() + " for host " + servers[i] ); - } - } - } - - private void populateConsistentBuckets() { - if ( log.isDebugEnabled() ) - log.debug( "++++ initializing internal hashing structure for consistent hashing" ); - - // store buckets in tree map - this.consistentBuckets = new TreeMap(); - - MessageDigest md5 = MD5.get(); - if ( this.totalWeight <= 0 && this.weights != null ) { - for ( int i = 0; i < this.weights.length; i++ ) - this.totalWeight += ( this.weights[i] == null ) ? 1 : this.weights[i]; - } - else if ( this.weights == null ) { - this.totalWeight = this.servers.length; - } - - for ( int i = 0; i < servers.length; i++ ) { - int thisWeight = 1; - if ( this.weights != null && this.weights[i] != null ) - thisWeight = this.weights[i]; - - double factor = Math.floor( ((double)(40 * this.servers.length * thisWeight)) / (double)this.totalWeight ); - - for ( long j = 0; j < factor; j++ ) { - byte[] d = md5.digest( ( servers[i] + "-" + j ).getBytes() ); - for ( int h = 0 ; h < 4; h++ ) { - Long k = - ((long)(d[3+h*4]&0xFF) << 24) - | ((long)(d[2+h*4]&0xFF) << 16) - | ((long)(d[1+h*4]&0xFF) << 8) - | ((long)(d[0+h*4]&0xFF)); - - consistentBuckets.put( k, servers[i] ); - if ( log.isDebugEnabled() ) - log.debug( "++++ added " + servers[i] + " to server bucket" ); - } - } - - // create initial connections - if ( log.isDebugEnabled() ) - log.debug( "+++ creating initial connections (" + initConn + ") for host: " + servers[i] ); - - for ( int j = 0; j < initConn; j++ ) { - SockIO socket = createSocket( servers[i] ); - if ( socket == null ) { - log.error( "++++ failed to create connection to: " + servers[i] + " -- only " + j + " created." ); - break; - } - - addSocketToPool( availPool, servers[i], socket ); - if ( log.isDebugEnabled() ) - log.debug( "++++ created and added socket: " + socket.toString() + " for host " + servers[i] ); - } - } - } - - /** - * Returns state of pool. - * - * @return true if initialized. - */ - public boolean isInitialized() { - return initialized; - } - - /** - * Creates a new SockIO obj for the given server. - * - * If server fails to connect, then return null and do not try
- * again until a duration has passed. This duration will grow
- * by doubling after each failed attempt to connect. - * - * @param host host:port to connect to - * @return SockIO obj or null if failed to create - */ - protected SockIO createSocket( String host ) { - - SockIO socket = null; - - // if host is dead, then we don't need to try again - // until the dead status has expired - // we do not try to put back in if failback is off - hostDeadLock.lock(); - try { - if ( failover && failback && hostDead.containsKey( host ) && hostDeadDur.containsKey( host ) ) { - - Date store = hostDead.get( host ); - long expire = hostDeadDur.get( host ).longValue(); - - if ( (store.getTime() + expire) > System.currentTimeMillis() ) - return null; - } - } - finally { - hostDeadLock.unlock(); - } - - try { - socket = new SockIO( this, host, this.socketTO, this.socketConnectTO, this.nagle ); - - if ( !socket.isConnected() ) { - log.error( "++++ failed to get SockIO obj for: " + host + " -- new socket is not connected" ); - deadPool.put( socket, ZERO ); - socket = null; - } - } - catch ( Exception ex ) { - log.error( "++++ failed to get SockIO obj for: " + host ); - log.error( ex.getMessage(), ex ); - socket = null; - } - - // if we failed to get socket, then mark - // host dead for a duration which falls off - hostDeadLock.lock(); - try { - if ( socket == null ) { - Date now = new Date(); - hostDead.put( host, now ); - - long expire = ( hostDeadDur.containsKey( host ) ) ? (((Long)hostDeadDur.get( host )).longValue() * 2) : 1000; - - if ( expire > MAX_RETRY_DELAY ) - expire = MAX_RETRY_DELAY; - - hostDeadDur.put( host, new Long( expire ) ); - if ( log.isDebugEnabled() ) - log.debug( "++++ ignoring dead host: " + host + " for " + expire + " ms" ); - - // also clear all entries for this host from availPool - clearHostFromPool( availPool, host ); - } - else { - if ( log.isDebugEnabled() ) - log.debug( "++++ created socket (" + socket.toString() + ") for host: " + host ); - if ( hostDead.containsKey( host ) || hostDeadDur.containsKey( host ) ) { - hostDead.remove( host ); - hostDeadDur.remove( host ); - } - } - } - finally { - hostDeadLock.unlock(); - } - - return socket; - } - - /** - * @param key - * @return - */ - public String getHost( String key ) { - return getHost( key, null ); - } - - /** - * Gets the host that a particular key / hashcode resides on. - * - * @param key - * @param hashcode - * @return - */ - public String getHost( String key, Integer hashcode ) { - SockIO socket = getSock( key, hashcode ); - String host = socket.getHost(); - socket.close(); - return host; - } - - /** - * Returns appropriate SockIO object given - * string cache key. - * - * @param key hashcode for cache key - * @return SockIO obj connected to server - */ - public SockIO getSock( String key ) { - return getSock( key, null ); - } - - /** - * Returns appropriate SockIO object given - * string cache key and optional hashcode. - * - * Trys to get SockIO from pool. Fails over - * to additional pools in event of server failure. - * - * @param key hashcode for cache key - * @param hashCode if not null, then the int hashcode to use - * @return SockIO obj connected to server - */ - public SockIO getSock( String key, Integer hashCode ) { - - if ( log.isDebugEnabled() ) - log.debug( "cache socket pick " + key + " " + hashCode ); - - if ( !this.initialized ) { - log.error( "attempting to get SockIO from uninitialized pool!" ); - return null; - } - - // if no servers return null - if ( ( this.hashingAlg == CONSISTENT_HASH && consistentBuckets.size() == 0 ) - || ( buckets != null && buckets.size() == 0 ) ) - return null; - - // if only one server, return it - if ( ( this.hashingAlg == CONSISTENT_HASH && consistentBuckets.size() == 1 ) - || ( buckets != null && buckets.size() == 1 ) ) { - - SockIO sock = ( this.hashingAlg == CONSISTENT_HASH ) - ? getConnection( consistentBuckets.get( consistentBuckets.firstKey() ) ) - : getConnection( buckets.get( 0 ) ); - - if ( sock != null && sock.isConnected() ) { - if ( aliveCheck ) { - if ( !sock.isAlive() ) { - sock.close(); - try { sock.trueClose(); } catch ( IOException ioe ) { log.error( "failed to close dead socket" ); } - sock = null; - } - } - } - else { - if ( sock != null ) { - deadPool.put( sock, ZERO ); - sock = null; - } - } - - return sock; - } - - // from here on, we are working w/ multiple servers - // keep trying different servers until we find one - // making sure we only try each server one time - Set tryServers = new HashSet( Arrays.asList( servers ) ); - - // get initial bucket - long bucket = getBucket( key, hashCode ); - String server = ( this.hashingAlg == CONSISTENT_HASH ) - ? consistentBuckets.get( bucket ) - : buckets.get( (int)bucket ); - - while ( !tryServers.isEmpty() ) { - - // try to get socket from bucket - SockIO sock = getConnection( server ); - - if ( log.isDebugEnabled() ) - log.debug( "cache choose " + server + " for " + key ); - - if ( sock != null && sock.isConnected() ) { - if ( aliveCheck ) { - if ( sock.isAlive() ) { - return sock; - } - else { - sock.close(); - try { sock.trueClose(); } catch ( IOException ioe ) { log.error( "failed to close dead socket" ); } - sock = null; - } - } - else { - return sock; - } - } - else { - if ( sock != null ) { - deadPool.put( sock, ZERO ); - sock = null; - } - } - - // if we do not want to failover, then bail here - if ( !failover ) - return null; - - // log that we tried - tryServers.remove( server ); - - if ( tryServers.isEmpty() ) - break; - - // if we failed to get a socket from this server - // then we try again by adding an incrementer to the - // current key and then rehashing - int rehashTries = 0; - while ( !tryServers.contains( server ) ) { - - String newKey = String.format( "%s%s", rehashTries, key ); - if ( log.isDebugEnabled() ) - log.debug( "rehashing with: " + newKey ); - - bucket = getBucket( newKey, null ); - server = ( this.hashingAlg == CONSISTENT_HASH ) - ? consistentBuckets.get( bucket ) - : buckets.get( (int)bucket ); - - rehashTries++; - } - } - - return null; - } - - /** - * Returns a SockIO object from the pool for the passed in host. - * - * Meant to be called from a more intelligent method
- * which handles choosing appropriate server
- * and failover. - * - * @param host host from which to retrieve object - * @return SockIO object or null if fail to retrieve one - */ - public SockIO getConnection( String host ) { - - if ( !this.initialized ) { - log.error( "attempting to get SockIO from uninitialized pool!" ); - return null; - } - - if ( host == null ) - return null; - - synchronized( this ) { - - // if we have items in the pool - // then we can return it - if ( availPool != null && !availPool.isEmpty() ) { - - // take first connected socket - Map aSockets = availPool.get( host ); - - if ( aSockets != null && !aSockets.isEmpty() ) { - - for ( Iterator i = aSockets.keySet().iterator(); i.hasNext(); ) { - SockIO socket = i.next(); - - if ( socket.isConnected() ) { - if ( log.isDebugEnabled() ) - log.debug( "++++ moving socket for host (" + host + ") to busy pool ... socket: " + socket ); - - // remove from avail pool - i.remove(); - - // add to busy pool - addSocketToPool( busyPool, host, socket ); - - // return socket - return socket; - } - else { - // add to deadpool for later reaping - deadPool.put( socket, ZERO ); - - // remove from avail pool - i.remove(); - } - } - } - } - } - - // create one socket -- let the maint thread take care of creating more - SockIO socket = createSocket( host ); - if ( socket != null ) { - synchronized( this ) { - addSocketToPool( busyPool, host, socket ); - } - } - - return socket; - } - - /** - * Adds a socket to a given pool for the given host. - * THIS METHOD IS NOT THREADSAFE, SO BE CAREFUL WHEN USING! - * - * Internal utility method. - * - * @param pool pool to add to - * @param host host this socket is connected to - * @param socket socket to add - */ - protected void addSocketToPool( Map> pool, String host, SockIO socket ) { - - if ( pool.containsKey( host ) ) { - Map sockets = pool.get( host ); - - if ( sockets != null ) { - sockets.put( socket, new Long( System.currentTimeMillis() ) ); - return; - } - } - - Map sockets = - new IdentityHashMap(); - - sockets.put( socket, new Long( System.currentTimeMillis() ) ); - pool.put( host, sockets ); - } - - /** - * Removes a socket from specified pool for host. - * THIS METHOD IS NOT THREADSAFE, SO BE CAREFUL WHEN USING! - * - * Internal utility method. - * - * @param pool pool to remove from - * @param host host pool - * @param socket socket to remove - */ - protected void removeSocketFromPool( Map> pool, String host, SockIO socket ) { - if ( pool.containsKey( host ) ) { - Map sockets = pool.get( host ); - if ( sockets != null ) - sockets.remove( socket ); - } - } - - /** - * Closes and removes all sockets from specified pool for host. - * THIS METHOD IS NOT THREADSAFE, SO BE CAREFUL WHEN USING! - * - * Internal utility method. - * - * @param pool pool to clear - * @param host host to clear - */ - protected void clearHostFromPool( Map> pool, String host ) { - - if ( pool.containsKey( host ) ) { - Map sockets = pool.get( host ); - - if ( sockets != null && sockets.size() > 0 ) { - for ( Iterator i = sockets.keySet().iterator(); i.hasNext(); ) { - SockIO socket = i.next(); - try { - socket.trueClose(); - } - catch ( IOException ioe ) { - log.error( "++++ failed to close socket: " + ioe.getMessage() ); - } - - i.remove(); - socket = null; - } - } - } - } - - /** - * Checks a SockIO object in with the pool. - * - * This will remove SocketIO from busy pool, and optionally
- * add to avail pool. - * - * @param socket socket to return - * @param addToAvail add to avail pool if true - */ - private void checkIn( SockIO socket, boolean addToAvail ) { - - String host = socket.getHost(); - if ( log.isDebugEnabled() ) - log.debug( "++++ calling check-in on socket: " + socket.toString() + " for host: " + host ); - - synchronized( this ) { - // remove from the busy pool - if ( log.isDebugEnabled() ) - log.debug( "++++ removing socket (" + socket.toString() + ") from busy pool for host: " + host ); - removeSocketFromPool( busyPool, host, socket ); - - if ( socket.isConnected() && addToAvail ) { - // add to avail pool - if ( log.isDebugEnabled() ) - log.debug( "++++ returning socket (" + socket.toString() + " to avail pool for host: " + host ); - addSocketToPool( availPool, host, socket ); - } - else { - deadPool.put( socket, ZERO ); - socket = null; - } - } - } - - /** - * Returns a socket to the avail pool. - * - * This is called from SockIO.close(). Calling this method
- * directly without closing the SockIO object first
- * will cause an IOException to be thrown. - * - * @param socket socket to return - */ - private void checkIn( SockIO socket ) { - checkIn( socket, true ); - } - - /** - * Closes all sockets in the passed in pool. - * - * Internal utility method. - * - * @param pool pool to close - */ - protected void closePool( Map> pool ) { - for ( Iterator i = pool.keySet().iterator(); i.hasNext(); ) { - String host = i.next(); - Map sockets = pool.get( host ); - - for ( Iterator j = sockets.keySet().iterator(); j.hasNext(); ) { - SockIO socket = j.next(); - - try { - socket.trueClose(); - } - catch ( IOException ioe ) { - log.error( "++++ failed to trueClose socket: " + socket.toString() + " for host: " + host ); - } - - j.remove(); - socket = null; - } - } - } - - /** - * Shuts down the pool. - * - * Cleanly closes all sockets.
- * Stops the maint thread.
- * Nulls out all internal maps
- */ - public void shutDown() { - synchronized( this ) { - if ( log.isDebugEnabled() ) - log.debug( "++++ SockIOPool shutting down..." ); - - if ( maintThread != null && maintThread.isRunning() ) { - // stop the main thread - stopMaintThread(); - - // wait for the thread to finish - while ( maintThread.isRunning() ) { - if ( log.isDebugEnabled() ) - log.debug( "++++ waiting for main thread to finish run +++" ); - try { Thread.sleep( 500 ); } catch ( Exception ex ) { } - } - } - - if ( log.isDebugEnabled() ) - log.debug( "++++ closing all internal pools." ); - closePool( availPool ); - closePool( busyPool ); - availPool = null; - busyPool = null; - buckets = null; - consistentBuckets = null; - hostDeadDur = null; - hostDead = null; - maintThread = null; - initialized = false; - if ( log.isDebugEnabled() ) - log.debug( "++++ SockIOPool finished shutting down." ); - } - } - - /** - * Starts the maintenance thread. - * - * This thread will manage the size of the active pool
- * as well as move any closed, but not checked in sockets
- * back to the available pool. - */ - protected void startMaintThread() { - - if ( maintThread != null ) { - - if ( maintThread.isRunning() ) { - log.error( "main thread already running" ); - } - else { - maintThread.start(); - } - } - else { - maintThread = new MaintThread( this ); - maintThread.setInterval( this.maintSleep ); - maintThread.start(); - } - } - - /** - * Stops the maintenance thread. - */ - protected void stopMaintThread() { - if ( maintThread != null && maintThread.isRunning() ) - maintThread.stopThread(); - } - - /** - * Runs self maintenance on all internal pools. - * - * This is typically called by the maintenance thread to manage pool size. - */ - protected void selfMaint() { - if ( log.isDebugEnabled() ) - log.debug( "++++ Starting self maintenance...." ); - - // go through avail sockets and create sockets - // as needed to maintain pool settings - Map needSockets = - new HashMap(); - - synchronized( this ) { - // find out how many to create - for ( Iterator i = availPool.keySet().iterator(); i.hasNext(); ) { - String host = i.next(); - Map sockets = availPool.get( host ); - - if ( log.isDebugEnabled() ) - log.debug( "++++ Size of avail pool for host (" + host + ") = " + sockets.size() ); - - // if pool is too small (n < minSpare) - if ( sockets.size() < minConn ) { - // need to create new sockets - int need = minConn - sockets.size(); - needSockets.put( host, need ); - } - } - } - - // now create - Map> newSockets = - new HashMap>(); - - for ( String host : needSockets.keySet() ) { - Integer need = needSockets.get( host ); - - if ( log.isDebugEnabled() ) - log.debug( "++++ Need to create " + need + " new sockets for pool for host: " + host ); - - Set newSock = new HashSet( need ); - for ( int j = 0; j < need; j++ ) { - SockIO socket = createSocket( host ); - - if ( socket == null ) - break; - - newSock.add( socket ); - } - - newSockets.put( host, newSock ); - } - - // synchronize to add and remove to/from avail pool - // as well as clean up the busy pool (no point in releasing - // lock here as should be quick to pool adjust and no - // blocking ops here) - synchronized( this ) { - for ( String host : newSockets.keySet() ) { - Set sockets = newSockets.get( host ); - for ( SockIO socket : sockets ) - addSocketToPool( availPool, host, socket ); - } - - for ( Iterator i = availPool.keySet().iterator(); i.hasNext(); ) { - String host = i.next(); - Map sockets = availPool.get( host ); - if ( log.isDebugEnabled() ) - log.debug( "++++ Size of avail pool for host (" + host + ") = " + sockets.size() ); - - if ( sockets.size() > maxConn ) { - // need to close down some sockets - int diff = sockets.size() - maxConn; - int needToClose = (diff <= poolMultiplier) - ? diff - : (diff) / poolMultiplier; - - if ( log.isDebugEnabled() ) - log.debug( "++++ need to remove " + needToClose + " spare sockets for pool for host: " + host ); - - for ( Iterator j = sockets.keySet().iterator(); j.hasNext(); ) { - if ( needToClose <= 0 ) - break; - - // remove stale entries - SockIO socket = j.next(); - long expire = sockets.get( socket ).longValue(); - - // if past idle time - // then close socket - // and remove from pool - if ( (expire + maxIdle) < System.currentTimeMillis() ) { - if ( log.isDebugEnabled() ) - log.debug( "+++ removing stale entry from pool as it is past its idle timeout and pool is over max spare" ); - - // remove from the availPool - deadPool.put( socket, ZERO ); - j.remove(); - needToClose--; - } - } - } - } - - // go through busy sockets and destroy sockets - // as needed to maintain pool settings - for ( Iterator i = busyPool.keySet().iterator(); i.hasNext(); ) { - - String host = i.next(); - Map sockets = busyPool.get( host ); - - if ( log.isDebugEnabled() ) - log.debug( "++++ Size of busy pool for host (" + host + ") = " + sockets.size() ); - - // loop through all connections and check to see if we have any hung connections - for ( Iterator j = sockets.keySet().iterator(); j.hasNext(); ) { - // remove stale entries - SockIO socket = j.next(); - long hungTime = sockets.get( socket ).longValue(); - - // if past max busy time - // then close socket - // and remove from pool - if ( (hungTime + maxBusyTime) < System.currentTimeMillis() ) { - log.error( "+++ removing potentially hung connection from busy pool ... socket in pool for " + (System.currentTimeMillis() - hungTime) + "ms" ); - - // remove from the busy pool - deadPool.put( socket, ZERO ); - j.remove(); - } - } - } - } - - // finally clean out the deadPool - Set toClose; - synchronized( deadPool ) { - toClose = deadPool.keySet(); - deadPool = new IdentityHashMap(); - } - - for ( SockIO socket : toClose ) { - try { - socket.trueClose( false ); - } - catch ( Exception ex ) { - log.error( "++++ failed to close SockIO obj from deadPool" ); - log.error( ex.getMessage(), ex ); - } - - socket = null; - } - - if ( log.isDebugEnabled() ) - log.debug( "+++ ending self maintenance." ); - } - - /** - * Class which extends thread and handles maintenance of the pool. - * - * @author greg whalin - * @version 1.5 - */ - protected static class MaintThread extends Thread { - - // logger - private static Logger log = - Logger.getLogger( MaintThread.class.getName() ); - - private SockIOPool pool; - private long interval = 1000 * 3; // every 3 seconds - private boolean stopThread = false; - private boolean running; - - protected MaintThread( SockIOPool pool ) { - this.pool = pool; - this.setDaemon( true ); - this.setName( "MaintThread" ); - } - - public void setInterval( long interval ) { this.interval = interval; } - - public boolean isRunning() { - return this.running; - } - - /** - * sets stop variable - * and interupts any wait - */ - public void stopThread() { - this.stopThread = true; - this.interrupt(); - } - - /** - * Start the thread. - */ - public void run() { - this.running = true; - - while ( !this.stopThread ) { - try { - Thread.sleep( interval ); - - // if pool is initialized, then - // run the maintenance method on itself - if ( pool.isInitialized() ) - pool.selfMaint(); - - } - catch ( Exception e ) { - break; - } - } - - this.running = false; - } - } - - /** - * MemCached client for Java, utility class for Socket IO. - * - * This class is a wrapper around a Socket and its streams. - * - * @author greg whalin - * @author Richard 'toast' Russo - * @version 1.5 - */ - public static class SockIO implements LineInputStream { - - // logger - private static Logger log = - Logger.getLogger( SockIO.class.getName() ); - - // pool - private SockIOPool pool; - - // data - private String host; - private Socket sock; - - private DataInputStream in; - private BufferedOutputStream out; - - /** - * creates a new SockIO object wrapping a socket - * connection to host:port, and its input and output streams - * - * @param pool Pool this object is tied to - * @param host host to connect to - * @param port port to connect to - * @param timeout int ms to block on data for read - * @param connectTimeout timeout (in ms) for initial connection - * @param noDelay TCP NODELAY option? - * @throws IOException if an io error occurrs when creating socket - * @throws UnknownHostException if hostname is invalid - */ - public SockIO( SockIOPool pool, String host, int port, int timeout, int connectTimeout, boolean noDelay ) throws IOException, UnknownHostException { - - this.pool = pool; - - // get a socket channel - sock = getSocket( host, port, connectTimeout ); - - if ( timeout >= 0 ) - sock.setSoTimeout( timeout ); - - // testing only - sock.setTcpNoDelay( noDelay ); - - // wrap streams - in = new DataInputStream( new BufferedInputStream( sock.getInputStream() ) ); - out = new BufferedOutputStream( sock.getOutputStream() ); - - this.host = host + ":" + port; - } - - /** - * creates a new SockIO object wrapping a socket - * connection to host:port, and its input and output streams - * - * @param host hostname:port - * @param timeout read timeout value for connected socket - * @param connectTimeout timeout for initial connections - * @param noDelay TCP NODELAY option? - * @throws IOException if an io error occurrs when creating socket - * @throws UnknownHostException if hostname is invalid - */ - public SockIO( SockIOPool pool, String host, int timeout, int connectTimeout, boolean noDelay ) throws IOException, UnknownHostException { - - this.pool = pool; - - String[] ip = host.split(":"); - - // get socket: default is to use non-blocking connect - sock = getSocket( ip[ 0 ], Integer.parseInt( ip[ 1 ] ), connectTimeout ); - - if ( timeout >= 0 ) - this.sock.setSoTimeout( timeout ); - - // testing only - sock.setTcpNoDelay( noDelay ); - - // wrap streams - in = new DataInputStream( new BufferedInputStream( sock.getInputStream() ) ); - out = new BufferedOutputStream( sock.getOutputStream() ); - - this.host = host; - } - - /** - * Method which gets a connection from SocketChannel. - * - * @param host host to establish connection to - * @param port port on that host - * @param timeout connection timeout in ms - * - * @return connected socket - * @throws IOException if errors connecting or if connection times out - */ - protected static Socket getSocket( String host, int port, int timeout ) throws IOException { - SocketChannel sock = SocketChannel.open(); - sock.socket().connect( new InetSocketAddress( host, port ), timeout ); - return sock.socket(); - } - - /** - * Lets caller get access to underlying channel. - * - * @return the backing SocketChannel - */ - public SocketChannel getChannel() { return sock.getChannel(); } - - /** - * returns the host this socket is connected to - * - * @return String representation of host (hostname:port) - */ - public String getHost() { return this.host; } - - /** - * closes socket and all streams connected to it - * - * @throws IOException if fails to close streams or socket - */ - public void trueClose() throws IOException { - trueClose( true ); - } - - /** - * closes socket and all streams connected to it - * - * @throws IOException if fails to close streams or socket - */ - public void trueClose( boolean addToDeadPool ) throws IOException { - if ( log.isDebugEnabled() ) - log.debug( "++++ Closing socket for real: " + toString() ); - - boolean err = false; - StringBuilder errMsg = new StringBuilder(); - - if ( in != null ) { - try { - in.close(); - } - catch( IOException ioe ) { - log.error( "++++ error closing input stream for socket: " + toString() + " for host: " + getHost() ); - log.error( ioe.getMessage(), ioe ); - errMsg.append( "++++ error closing input stream for socket: " + toString() + " for host: " + getHost() + "\n" ); - errMsg.append( ioe.getMessage() ); - err = true; - } - } - - if ( out != null ) { - try { - out.close(); - } - catch ( IOException ioe ) { - log.error( "++++ error closing output stream for socket: " + toString() + " for host: " + getHost() ); - log.error( ioe.getMessage(), ioe ); - errMsg.append( "++++ error closing output stream for socket: " + toString() + " for host: " + getHost() + "\n" ); - errMsg.append( ioe.getMessage() ); - err = true; - } - } - - if ( sock != null ) { - try { - sock.close(); - } - catch ( IOException ioe ) { - log.error( "++++ error closing socket: " + toString() + " for host: " + getHost() ); - log.error( ioe.getMessage(), ioe ); - errMsg.append( "++++ error closing socket: " + toString() + " for host: " + getHost() + "\n" ); - errMsg.append( ioe.getMessage() ); - err = true; - } - } - - // check in to pool - if ( addToDeadPool && sock != null ) - pool.checkIn( this, false ); - - in = null; - out = null; - sock = null; - - if ( err ) - throw new IOException( errMsg.toString() ); - } - - /** - * sets closed flag and checks in to connection pool - * but does not close connections - */ - void close() { - // check in to pool - if ( log.isDebugEnabled() ) - log.debug("++++ marking socket (" + this.toString() + ") as closed and available to return to avail pool"); - pool.checkIn( this ); - } - - /** - * checks if the connection is open - * - * @return true if connected - */ - boolean isConnected() { - return ( sock != null && sock.isConnected() ); - } - - /* - * checks to see that the connection is still working - * - * @return true if still alive - */ - boolean isAlive() { - - if ( !isConnected() ) - return false; - - // try to talk to the server w/ a dumb query to ask its version - try { - this.write( "version\r\n".getBytes() ); - this.flush(); - String response = this.readLine(); - } - catch ( IOException ex ) { - return false; - } - - return true; - } - - /** - * reads a line - * intentionally not using the deprecated readLine method from DataInputStream - * - * @return String that was read in - * @throws IOException if io problems during read - */ - public String readLine() throws IOException { - if ( sock == null || !sock.isConnected() ) { - log.error( "++++ attempting to read from closed socket" ); - throw new IOException( "++++ attempting to read from closed socket" ); - } - - byte[] b = new byte[1]; - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - boolean eol = false; - - while ( in.read( b, 0, 1 ) != -1 ) { - - if ( b[0] == 13 ) { - eol = true; - } - else { - if ( eol ) { - if ( b[0] == 10 ) - break; - - eol = false; - } - } - - // cast byte into char array - bos.write( b, 0, 1 ); - } - - if ( bos == null || bos.size() <= 0 ) { - throw new IOException( "++++ Stream appears to be dead, so closing it down" ); - } - - // else return the string - return bos.toString().trim(); - } - - /** - * reads up to end of line and returns nothing - * - * @throws IOException if io problems during read - */ - public void clearEOL() throws IOException { - if ( sock == null || !sock.isConnected() ) { - log.error( "++++ attempting to read from closed socket" ); - throw new IOException( "++++ attempting to read from closed socket" ); - } - - byte[] b = new byte[1]; - boolean eol = false; - while ( in.read( b, 0, 1 ) != -1 ) { - - // only stop when we see - // \r (13) followed by \n (10) - if ( b[0] == 13 ) { - eol = true; - continue; - } - - if ( eol ) { - if ( b[0] == 10 ) - break; - - eol = false; - } - } - } - - /** - * reads length bytes into the passed in byte array from dtream - * - * @param b byte array - * @throws IOException if io problems during read - */ - public int read( byte[] b ) throws IOException { - if ( sock == null || !sock.isConnected() ) { - log.error( "++++ attempting to read from closed socket" ); - throw new IOException( "++++ attempting to read from closed socket" ); - } - - int count = 0; - while ( count < b.length ) { - int cnt = in.read( b, count, (b.length - count) ); - count += cnt; - } - - return count; - } - - /** - * flushes output stream - * - * @throws IOException if io problems during read - */ - void flush() throws IOException { - if ( sock == null || !sock.isConnected() ) { - log.error( "++++ attempting to write to closed socket" ); - throw new IOException( "++++ attempting to write to closed socket" ); - } - out.flush(); - } - - /** - * writes a byte array to the output stream - * - * @param b byte array to write - * @throws IOException if an io error happens - */ - void write( byte[] b ) throws IOException { - if ( sock == null || !sock.isConnected() ) { - log.error( "++++ attempting to write to closed socket" ); - throw new IOException( "++++ attempting to write to closed socket" ); - } - out.write( b ); - } - - /** - * use the sockets hashcode for this object - * so we can key off of SockIOs - * - * @return int hashcode - */ - public int hashCode() { - return ( sock == null ) ? 0 : sock.hashCode(); - } - - /** - * returns the string representation of this socket - * - * @return string - */ - public String toString() { - return ( sock == null ) ? "" : sock.toString(); - } - - /** - * Hack to reap any leaking children. - */ - protected void finalize() throws Throwable { - try { - if ( sock != null ) { - log.error( "++++ closing potentially leaked socket in finalize" ); - sock.close(); - sock = null; - } - } - catch ( Throwable t ) { - log.error( t.getMessage(), t ); - } - finally { - super.finalize(); - } - } - } -} diff --git a/src/com/meetup/memcached/test/MemcachedBench.java b/src/com/meetup/memcached/test/MemcachedBench.java deleted file mode 100644 index 9f0491f..0000000 --- a/src/com/meetup/memcached/test/MemcachedBench.java +++ /dev/null @@ -1,99 +0,0 @@ -/** - * 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 - */ -package com.meetup.memcached.test; - -import com.meetup.memcached.*; -import java.util.*; -import org.apache.log4j.Level; -import org.apache.log4j.Logger; -import org.apache.log4j.BasicConfigurator; - -public class MemcachedBench { - - // logger - private static Logger log = - Logger.getLogger( MemcachedBench.class.getName() ); - - public static void main(String[] args) { - - BasicConfigurator.configure(); - org.apache.log4j.Logger.getRootLogger().setLevel( Level.OFF ); - - int runs = Integer.parseInt(args[0]); - int start = Integer.parseInt(args[1]); - - String[] serverlist = { "192.168.1.50:1624" }; - - // initialize the pool for memcache servers - SockIOPool pool = SockIOPool.getInstance( "test" ); - pool.setServers(serverlist); - - pool.setInitConn( 100 ); - pool.setMinConn( 100 ); - pool.setMaxConn( 500 ); - pool.setMaintSleep( 20 ); - - pool.setNagle( false ); - pool.initialize(); - - // get client instance - MemcachedClient mc = new MemcachedClient( "test" ); - mc.setCompressEnable( false ); - - String keyBase = "testKey"; - String object = "This is a test of an object blah blah es, serialization does not seem to slow things down so much. The gzip compression is horrible horrible performance, so we only use it for very large objects. I have not done any heavy benchmarking recently"; - - long begin = System.currentTimeMillis(); - for (int i = start; i < start+runs; i++) { - mc.set(keyBase + i, object); - } - long end = System.currentTimeMillis(); - long time = end - begin; - System.out.println(runs + " sets: " + time + "ms"); - - begin = System.currentTimeMillis(); - for (int i = start; i < start+runs; i++) { - String str = (String) mc.get(keyBase + i); - } - end = System.currentTimeMillis(); - time = end - begin; - System.out.println(runs + " gets: " + time + "ms"); - - String[] keys = new String[ runs ]; - int j = 0; - for (int i = start; i < start+runs; i++) { - keys[ j ] = keyBase + i; - j++; - } - begin = System.currentTimeMillis(); - Map vals = mc.getMulti( keys ); - end = System.currentTimeMillis(); - time = end - begin; - System.out.println(runs + " getMulti: " + time + "ms"); - - begin = System.currentTimeMillis(); - for (int i = start; i < start+runs; i++) { - mc.delete( keyBase + i ); - } - end = System.currentTimeMillis(); - time = end - begin; - System.out.println(runs + " deletes: " + time + "ms"); - - SockIOPool.getInstance( "test" ).shutDown(); - } -} diff --git a/src/com/meetup/memcached/test/MemcachedTest.java b/src/com/meetup/memcached/test/MemcachedTest.java deleted file mode 100644 index f695e96..0000000 --- a/src/com/meetup/memcached/test/MemcachedTest.java +++ /dev/null @@ -1,151 +0,0 @@ -/** - * 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 - */ -package com.meetup.memcached.test; - -import com.meetup.memcached.*; -import java.util.*; - -public class MemcachedTest { - - // store results from threads - private static Hashtable threadInfo = - new Hashtable(); - - /** - * This runs through some simple tests of the MemcacheClient. - * - * Command line args: - * args[0] = number of threads to spawn - * args[1] = number of runs per thread - * args[2] = size of object to store - * - * @param args the command line arguments - */ - public static void main(String[] args) { - - String[] serverlist = { "cache1.int.meetup.com:12345", "cache0.int.meetup.com:12345" }; - - // initialize the pool for memcache servers - SockIOPool pool = SockIOPool.getInstance(); - pool.setServers( serverlist ); - - pool.setInitConn(5); - pool.setMinConn(5); - pool.setMaxConn(50); - pool.setMaintSleep(30); - - pool.setNagle(false); - pool.initialize(); - - int threads = Integer.parseInt(args[0]); - int runs = Integer.parseInt(args[1]); - int size = 1024 * Integer.parseInt(args[2]); // how many kilobytes - - // get object to store - int[] obj = new int[size]; - for (int i = 0; i < size; i++) { - obj[i] = i; - } - - String[] keys = new String[size]; - for (int i = 0; i < size; i++) { - keys[i] = "test_key" + i; - } - - for (int i = 0; i < threads; i++) { - bench b = new bench(runs, i, obj, keys); - b.start(); - } - - int i = 0; - while (i < threads) { - if (threadInfo.containsKey(new Integer(i))) { - System.out.println( threadInfo.get( new Integer( i ) ) ); - i++; - } - else { - try { - Thread.sleep(1000); - } - catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - - pool.shutDown(); - System.exit(1); - } - - /** - * Test code per thread. - */ - private static class bench extends Thread { - private int runs; - private int threadNum; - private int[] object; - private String[] keys; - private int size; - - public bench(int runs, int threadNum, int[] object, String[] keys) { - this.runs = runs; - this.threadNum = threadNum; - this.object = object; - this.keys = keys; - this.size = object.length; - } - - public void run() { - - StringBuilder result = new StringBuilder(); - - // get client instance - MemcachedClient mc = new MemcachedClient(); - mc.setCompressEnable(false); - mc.setCompressThreshold(0); - - // time deletes - long start = System.currentTimeMillis(); - for (int i = 0; i < runs; i++) { - mc.delete(keys[i]); - } - long elapse = System.currentTimeMillis() - start; - float avg = (float) elapse / runs; - result.append("\nthread " + threadNum + ": runs: " + runs + " deletes of obj " + (size/1024) + "KB -- avg time per req " + avg + " ms (total: " + elapse + " ms)"); - - // time stores - start = System.currentTimeMillis(); - for (int i = 0; i < runs; i++) { - mc.set(keys[i], object); - } - elapse = System.currentTimeMillis() - start; - avg = (float) elapse / runs; - result.append("\nthread " + threadNum + ": runs: " + runs + " stores of obj " + (size/1024) + "KB -- avg time per req " + avg + " ms (total: " + elapse + " ms)"); - - start = System.currentTimeMillis(); - for (int i = 0; i < runs; i++) { - mc.get(keys[i]); - } - elapse = System.currentTimeMillis() - start; - avg = (float) elapse / runs; - result.append("\nthread " + threadNum + ": runs: " + runs + " gets of obj " + (size/1024) + "KB -- avg time per req " + avg + " ms (total: " + elapse + " ms)"); - - threadInfo.put(new Integer(threadNum), result); - } - } -} diff --git a/src/com/meetup/memcached/test/TestMemcached.java b/src/com/meetup/memcached/test/TestMemcached.java deleted file mode 100644 index b0a9bae..0000000 --- a/src/com/meetup/memcached/test/TestMemcached.java +++ /dev/null @@ -1,63 +0,0 @@ -/** - * 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 - */ -package com.meetup.memcached.test; - -import com.meetup.memcached.*; -import org.apache.log4j.*; - -public class TestMemcached { - public static void main(String[] args) { - // memcached should be running on port 11211 but NOT on 11212 - - BasicConfigurator.configure(); - String[] servers = { "192.168.1.1:1624", "192.168.1.1:1625" }; - SockIOPool pool = SockIOPool.getInstance(); - pool.setServers( servers ); - pool.setFailover( true ); - pool.setInitConn( 10 ); - pool.setMinConn( 5 ); - pool.setMaxConn( 250 ); - pool.setMaintSleep( 30 ); - pool.setNagle( false ); - pool.setSocketTO( 3000 ); - pool.setAliveCheck( true ); - pool.initialize(); - - MemcachedClient mcc = new MemcachedClient(); - - // turn off most memcached client logging: - com.meetup.memcached.Logger.getLogger( MemcachedClient.class.getName() ).setLevel( com.meetup.memcached.Logger.LEVEL_WARN ); - - for ( int i = 0; i < 10; i++ ) { - boolean success = mcc.set( "" + i, "Hello!" ); - String result = (String)mcc.get( "" + i ); - System.out.println( String.format( "set( %d ): %s", i, success ) ); - System.out.println( String.format( "get( %d ): %s", i, result ) ); - } - - System.out.println( "\n\t -- sleeping --\n" ); - try { Thread.sleep( 10000 ); } catch ( Exception ex ) { } - - for ( int i = 0; i < 10; i++ ) { - boolean success = mcc.set( "" + i, "Hello!" ); - String result = (String)mcc.get( "" + i ); - System.out.println( String.format( "set( %d ): %s", i, success ) ); - System.out.println( String.format( "get( %d ): %s", i, result ) ); - } - } -} diff --git a/src/com/meetup/memcached/test/UnitTests.java b/src/com/meetup/memcached/test/UnitTests.java deleted file mode 100644 index f9bc089..0000000 --- a/src/com/meetup/memcached/test/UnitTests.java +++ /dev/null @@ -1,403 +0,0 @@ -/** - * 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 Kevin Burton - * @author greg whalin - */ -package com.meetup.memcached.test; - -import com.meetup.memcached.*; -import java.util.*; -import java.io.Serializable; - -import org.apache.log4j.Level; -import org.apache.log4j.Logger; -import org.apache.log4j.BasicConfigurator; - -public class UnitTests { - - // logger - private static Logger log = - Logger.getLogger( UnitTests.class.getName() ); - - public static MemcachedClient mc = null; - - public static void test1() { - mc.set( "foo", Boolean.TRUE ); - Boolean b = (Boolean)mc.get( "foo" ); - assert b.booleanValue(); - log.error( "+ store/retrieve Boolean type test passed" ); - } - - public static void test2() { - mc.set( "foo", new Integer( Integer.MAX_VALUE ) ); - Integer i = (Integer)mc.get( "foo" ); - assert i.intValue() == Integer.MAX_VALUE; - log.error( "+ store/retrieve Integer type test passed" ); - } - - public static void test3() { - String input = "test of string encoding"; - mc.set( "foo", input ); - String s = (String)mc.get( "foo" ); - assert s.equals( input ); - log.error( "+ store/retrieve String type test passed" ); - } - - public static void test4() { - mc.set( "foo", new Character( 'z' ) ); - Character c = (Character)mc.get( "foo" ); - assert c.charValue() == 'z'; - log.error( "+ store/retrieve Character type test passed" ); - } - - public static void test5() { - mc.set( "foo", new Byte( (byte)127 ) ); - Byte b = (Byte)mc.get( "foo" ); - assert b.byteValue() == 127; - log.error( "+ store/retrieve Byte type test passed" ); - } - - public static void test6() { - mc.set( "foo", new StringBuffer( "hello" ) ); - StringBuffer o = (StringBuffer)mc.get( "foo" ); - assert o.toString().equals( "hello" ); - log.error( "+ store/retrieve StringBuffer type test passed" ); - } - - public static void test7() { - mc.set( "foo", new Short( (short)100 ) ); - Short o = (Short)mc.get( "foo" ); - assert o.shortValue() == 100; - log.error( "+ store/retrieve Short type test passed" ); - } - - public static void test8() { - mc.set( "foo", new Long( Long.MAX_VALUE ) ); - Long o = (Long)mc.get( "foo" ); - assert o.longValue() == Long.MAX_VALUE; - log.error( "+ store/retrieve Long type test passed" ); - } - - public static void test9() { - mc.set( "foo", new Double( 1.1 ) ); - Double o = (Double)mc.get( "foo" ); - assert o.doubleValue() == 1.1; - log.error( "+ store/retrieve Double type test passed" ); - } - - public static void test10() { - mc.set( "foo", new Float( 1.1f ) ); - Float o = (Float)mc.get( "foo" ); - assert o.floatValue() == 1.1f; - log.error( "+ store/retrieve Float type test passed" ); - } - - public static void test11() { - mc.set( "foo", new Integer( 100 ), new Date( System.currentTimeMillis() )); - try { Thread.sleep( 1000 ); } catch ( Exception ex ) { } - assert mc.get( "foo" ) == null; - log.error( "+ store/retrieve w/ expiration test passed" ); - } - - public static void test12() { - long i = 0; - mc.storeCounter("foo", i); - mc.incr("foo"); // foo now == 1 - mc.incr("foo", (long)5); // foo now == 6 - long j = mc.decr("foo", (long)2); // foo now == 4 - assert j == 4; - assert j == mc.getCounter( "foo" ); - log.error( "+ incr/decr test passed" ); - } - - public static void test13() { - Date d1 = new Date(); - mc.set("foo", d1); - Date d2 = (Date) mc.get("foo"); - assert d1.equals( d2 ); - log.error( "+ store/retrieve Date type test passed" ); - } - - public static void test14() { - assert !mc.keyExists( "foobar123" ); - mc.set( "foobar123", new Integer( 100000) ); - assert mc.keyExists( "foobar123" ); - log.error( "+ store/retrieve test passed" ); - - assert !mc.keyExists( "counterTest123" ); - mc.storeCounter( "counterTest123", 0 ); - assert mc.keyExists( "counterTest123" ); - log.error( "+ counter store test passed" ); - } - - public static void test15() { - - Map stats = mc.statsItems(); - assert stats != null; - - stats = mc.statsSlabs(); - assert stats != null; - - log.error( "+ stats test passed" ); - } - - public static void test16() { - assert !mc.set( "foo", null ); - log.error( "+ invalid data store [null] test passed" ); - } - - public static void test17() { - mc.set( "foo bar", Boolean.TRUE ); - Boolean b = (Boolean)mc.get( "foo bar" ); - assert b.booleanValue(); - log.error( "+ store/retrieve Boolean type test passed" ); - } - - public static void test18() { - long i = 0; - mc.addOrIncr( "foo" ); // foo now == 0 - mc.incr( "foo" ); // foo now == 1 - mc.incr( "foo", (long)5 ); // foo now == 6 - - mc.addOrIncr( "foo" ); // foo now 7 - - long j = mc.decr( "foo", (long)3 ); // foo now == 4 - assert j == 4; - assert j == mc.getCounter( "foo" ); - - log.error( "+ incr/decr test passed" ); - } - - public static void test19() { - int max = 100; - String[] keys = new String[ max ]; - for ( int i=0; i results = mc.getMulti( keys ); - for ( int i=0; i results = mc.getMulti( keys ); - for ( int i=0; i results = mc.getMulti( allKeys ); - - assert allKeys.length == results.size(); - for ( String key : setKeys ) { - String val = (String)results.get( key ); - assert key.equals( val ); - } - - log.error( "+ getMulti w/ keys that don't exist test passed" ); - } - - public static void runAlTests( MemcachedClient mc ) { - test14(); - for ( int t = 0; t < 2; t++ ) { - mc.setCompressEnable( ( t&1 ) == 1 ); - - test1(); - test2(); - test3(); - test4(); - test5(); - test6(); - test7(); - test8(); - test9(); - test10(); - test11(); - test12(); - test13(); - test15(); - test16(); - test17(); - test21(); - test22(); - test23(); - test24(); - - for ( int i = 0; i < 3; i++ ) - test19(); - - test20( 8191, 1, 0 ); - test20( 8192, 1, 0 ); - test20( 8193, 1, 0 ); - - test20( 16384, 100, 0 ); - test20( 17000, 128, 0 ); - - test20( 128*1024, 1023, 0 ); - test20( 128*1024, 1023, 1 ); - test20( 128*1024, 1024, 0 ); - test20( 128*1024, 1024, 1 ); - - test20( 128*1024, 1023, 0 ); - test20( 128*1024, 1023, 1 ); - test20( 128*1024, 1024, 0 ); - test20( 128*1024, 1024, 1 ); - - test20( 900*1024, 32*1024, 0 ); - test20( 900*1024, 32*1024, 1 ); - } - - } - - /** - * This runs through some simple tests of the MemcacheClient. - * - * Command line args: - * args[0] = number of threads to spawn - * args[1] = number of runs per thread - * args[2] = size of object to store - * - * @param args the command line arguments - */ - public static void main(String[] args) { - - BasicConfigurator.configure(); - org.apache.log4j.Logger.getRootLogger().setLevel( Level.WARN ); - - if ( !UnitTests.class.desiredAssertionStatus() ) { - System.err.println( "WARNING: assertions are disabled!" ); - try { Thread.sleep( 3000 ); } catch ( InterruptedException e ) {} - } - - String[] serverlist = { - "192.168.1.50:1620", - "192.168.1.50:1621", - "192.168.1.50:1622", - "192.168.1.50:1623", - "192.168.1.50:1624", - "192.168.1.50:1625", - "192.168.1.50:1626", - "192.168.1.50:1627", - "192.168.1.50:1628", - "192.168.1.50:1629" - }; - - Integer[] weights = { 1, 1, 1, 1, 10, 5, 1, 1, 1, 3 }; - - if ( args.length > 0 ) - serverlist = args; - - // initialize the pool for memcache servers - SockIOPool pool = SockIOPool.getInstance( "test" ); - pool.setServers( serverlist ); - pool.setWeights( weights ); - pool.setMaxConn( 250 ); - pool.setNagle( false ); - pool.setHashingAlg( SockIOPool.CONSISTENT_HASH ); - pool.initialize(); - - mc = new MemcachedClient( "test" ); - runAlTests( mc ); - } - - /** - * Class for testing serializing of objects. - * - * @author $Author: $ - * @version $Revision: $ $Date: $ - */ - public static final class TestClass implements Serializable { - - private String field1; - private String field2; - private Integer field3; - - public TestClass( String field1, String field2, Integer field3 ) { - this.field1 = field1; - this.field2 = field2; - this.field3 = field3; - } - - public String getField1() { return this.field1; } - public String getField2() { return this.field2; } - public Integer getField3() { return this.field3; } - - public boolean equals( Object o ) { - if ( this == o ) return true; - if ( !( o instanceof TestClass ) ) return false; - - TestClass obj = (TestClass)o; - - return ( ( this.field1 == obj.getField1() || ( this.field1 != null && this.field1.equals( obj.getField1() ) ) ) - && ( this.field2 == obj.getField2() || ( this.field2 != null && this.field2.equals( obj.getField2() ) ) ) - && ( this.field3 == obj.getField3() || ( this.field3 != null && this.field3.equals( obj.getField3() ) ) ) ); - } - } -} From 000559515507c8c67548316358e2670a81ab88c6 Mon Sep 17 00:00:00 2001 From: triompha Date: Tue, 24 Dec 2013 18:12:00 +0800 Subject: [PATCH 2/4] code subt --- pom.xml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 pom.xml diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..def7349 --- /dev/null +++ b/pom.xml @@ -0,0 +1,14 @@ + + 4.0.0 + com.triompha + Memcached-Java-Client + 0.0.1-SNAPSHOT + + + org.slf4j + slf4j-log4j12 + 1.6.1 + + + + \ No newline at end of file From ff4f0711bcb0f99b796d30f4f373646d30281afa Mon Sep 17 00:00:00 2001 From: triompha Date: Tue, 24 Dec 2013 18:13:04 +0800 Subject: [PATCH 3/4] code submit --- lib/log4j.jar | Bin 352291 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 lib/log4j.jar diff --git a/lib/log4j.jar b/lib/log4j.jar deleted file mode 100644 index a6568b01a2179f5b20473a1dcee4c67da062b327..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 352291 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⁡~$Py3|DlF$%lyfCnPAu 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 GUgIOVjX^YWJ+#$3+>)do}`%=eHNS)`yb%O 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%0A{%xB*;LAuljzH;^BK@v*W4rL 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�BSH3nMV(Fk2Q&+e@Gn zTS(oI7>fo%v<3nG>H}866wLjz$+)cBv_C617r#6ry*7^ z)mmkY8@i#c{Tb#5AlrP1D`%#pFUERwfx9y!U_W5@&EOA%a)+fWpFmIW`%971BVs(T z&%LRc!C}DIC%<@|IrSYTFj!jt8rcHhl%0!ib8smBUoYEO+nZK9+B_$ye5Gx_?HUV3 zl5r2TI(w6{@?S?CZ|pB3a&}-%vUp{lFT(~7>pRC6@AYk~&+<(XKY^Biw`er4J`-A} z*85R689`MmFciwM_uN7?ldzk{RRnA6@zn;PN@g%@T9K>K89KeLSS(<<%zOPBS&}0ohxy9nEHM6Fy@ngao^#oOM<<@q++!-_%~vuns_uaEnn5@ zj)T7r5YKg<^s%Zy_ycdT9yPh`S-a+)m4E`njSzkZLLvBZ+hA-1CvRV;KkRa2*V;Jm zFJ{LZk&q zpQ$Fa63jIj(_H*?uRYhYA$3jMUu7w~^a18D!1YDt!#E@nSi;;DPf-}IM7MjyX}Xi^ zepTsyML+J|4_CAqEnG^FAj+Y?EsI}s=#y{_UUBX4dpKo6j0*la7@MrCdRpeQtkMuf z_K+8;Pek{bEa`g~*X@B;6q_o-tq6bmt|8Q}2w&cVJ9n=k^d`zxq%%hJxkD|POH1_F zAt@eAk?(a({l%jx@L-5VMq3!{7EiI<`_|yiC4Wuo4e3FHM35BJ92HPbh8yK3ZfRKe zi--;u!%-3@42H8H#1crN*-;0GMEf@yV?{_kaE#UN6Yvm^hEHd}28iDP%PlZAs)M{* z09^$#(qX^;p+s&)UmdieDwtDP#gJabkcxY$%zL`~b0faQE#-Mn7Q$D7yN5f$D{WMa zYbg1ePdG5S+ax15jh7O;{sk6#=yOWJs5e%Or+s)3lk&8=g-3;f|bXTBlW?iSToAZKPK*B$C6eNL`H4&sM% zVQw1qW_@t(8|o8u(w(3bnD)|*!rrv&(kqT=;eK6_gE)W*=-JW-a*!;FA+}8N^7oI zSny%ALfdH`wY#TXp0|!ST(0rnH~o8V(KBX)TG@)2Fb2s{P3oM;17pQ@EFlX?gUq4_ zJ5epnY=t69i#SV(f=kWz(kXSOrO)^0-h!L>sTy+=u1bqvqC%(l+8}dM+Y)H~g(9Y{ zR;WvKZjC`1r1-v52-tjp43hR_lsjo7yJ@RyCC|aA^yBv`vAaWSWp1M>yTdg|JmtX>Qh-nFVc(d!f#ux*aGadNv2)b0s_b60i=My_$OibOqme0h-YQ(A*AS zwL9_2^#&xx=BAAj6Uw6_OsqV}mCX1o7Pb`U_SI(qJLt~j=D=vOMxO!CvGHH^S`JxC zuudqDi~P%mHPJTU4kJUCmFIULa#fDKEcWAz4BIK1WYlc&wq&QA>oKPc){~2HtS;Gs zTmpMwg=sResd$xX>)tRo;4mo?7I+sCP|Z7msyF4jeUWqoUKh>(&@YQ zzhy5aARtg(w@EoZqRKWqvZHnFDDPgpag(r5Q2w@h!o1yh2ha=Jw@h%BX!&f6rQAn( z7T64oi#fc+SsmVO5+=|g?to-A?hgt zug_nArf61vsz{{4+jx-#8`j1^+yLRBJ9EoR-HLoj4#(b>#pFVMD%`T*fnY8=0eOED z%7B<$nR6$rPhoR^s!fCNiAZk0=xs{L>fOFMCrm879<-r-!;QqhzT)SA;Bh3~*;^jn zB`38Ao3ay6*hKD{$UdqxWnLs9wc&G5kzwMksGVo-Enxns(uwqjOqc{>j;#gUqnP`VF&EXI&{Bot5Y`J!mm|O<>oZ>#rjx@&8tnRD0|z$s z;UyTuyLe-%_j?L{?~~~An1v!9J{LDDfA0`y4{i*c*Ewcx_iVA-Zf`zFES@RfhQMsD zmh%uy=|1MMWdSys!dI8im->mlV8`na1>s6;!tsQlV~?G!Bj(^?hyB;s%`eFQIkvKf z{VjJT_6<8~Wf9{=n!FJ6uYj9QHuSMmsCD|RmXm|GLlvy3&2H2UY?;?!=&ANr4!mab zprtG=S}phZ&`TN#tXr^aY{mPl@?myAypmTd3}M`k*>Q%R=f>sW_m%6))9q;keCgK7 z`RSh^I=q!lp1->^e$NcF3wYs4E4PJ~m{z>Hv2yI%1sApczEM5nUanxOVB6k@Y<)-UFJHfzf7$4}{U7azg~XGlNUB za5o?~@0lWRPYOfhSv}4zxH&KS&GLCx`=}stg>{f_r4zQO9Yq~ZZ~-pP=kPw|9_a1! zhnN|TGTK1uvyck~?wfwaEj{)CPXx(>PA67>Vf0Rms-pzo21~HFI~o9?&8u{z>#G0q zes{q%qD?jSvGW5J0JaC;V5$5Za4y7F z<2-*AQ^|+sI<)8s2(+sbMzg*4gRpaA)9Q_ZzH0krse5Tof~7$N@K_BZ_5DH6Noi2a zt~(^I+nepv>wa^&77mS4C5?byekiB0P82I}<0vry&|kClSyL-#e=Zfn)=?d6n2{rS zUWf~mcnnk$z5B)!lGtrf*o5?+Vk(_K_Pi+Z5^MUIIKF+l3id$<q8&;A8h#IL1AExs)8oH_vmq1NH5 z^Mqm1^Y0?^SK?z&AZd!f({J0;LyfQta?|-+HNs?bOKAX65t^WT`@sZIb3~3$k>e~M zM{k=lD}W#guF8Y0&fd&xE!uSVpP_RTP0n1uza@3#|8|Oi^4}^>l+9cm+?-9!{&l(+ zC$9g)*L0}o)U1`Z7}b*McNw8H!^$Ac+b<&QMDii9L}MwRIXkRz_DlQ5&9{?nRTMaJ z6a%@B!YGTUC1o9^+x!j-^X}K|>#WAl*Yk4^Q74FJR8mJ5aOM`&jXpc9b!g*}NHHOC zCGl{gub~B=EF&eG(`&cp>P)%Em3p|wwrN5>cG-R|!{0|R6tMLcOH@+FZ2bokpYqx3 zwGThnyKLDWp2F>Mj4vIAvRDd->x#A9RTevb?nhPHRlepGw`^aQ`wm-Hs%auOyw(aV& zZJS-jt+n^sH%{ERXGeVJp7X_=G5@{qpE;h)Co{*$;n=Y5Y8G;i6Rh>E^>IG%Ekqb7 zoKS>iLgP@b&T+`l_hy{rLgi`j-d)=^7Oqid33lpbYAcw7&7v5wQKbwv+t->Ytd}EU zh%iEAk~6v>HXk((UMzDD)Q~nnbH!k#!vjjgc)U068p47dcszWvXv`I-lNaw>n%9~6 z__mGqnECkiLR>s)MZ$N>??rVA1-fyi@=51Ok7PB|Lk{4}D>myDZWL~g`jA<$=dhaNC|Ci`U-y6!M?fO--7AhEhog8Q zN@=_(^NtTqJ|UAb5mN#edv%To>+c~!%)gZ!|NB9~zaXsq&7gAp7mhV;5#`}WzQD+##x*Nycp&I#QpHT5gqfXP$Qo;J)ohE(C2Wg(G7pI80Mt zvc4v;eLi0wapaZkbb})^ZE43x_roJA)Z*p&XW2-$so#pJ^9c2W2)b?5f}eZ|G{~8(&2S$~ZvCup|6*3Y6!*KmS1YFw0H zG{0`FU3++AZz8q;1=Dd28jG<9cm=<+x4=&~x=XS{-;cOqk-BQrJo^a}1`owzz}6LLj~Jz5yNv(A_n8MTb~S*e@+tL4Mh2m*7H_TI0kx4(Q?3ox zM6{tw?ZW|a^>&0)206z+m`gP*_6U(IC{~2>kDkRMM-EvRdw7VAu;3J%1>KwuMuL0G zdFx;Y4GU%2%G=z4K!*8U>y&Tw8CpW711=il;icYQ___5*nCyKt=asn}w0W15*K>ZEw$?U%X*F;NRdeU_TA&*0WfFPOU~v8Yct3)&rn znH5O@*ITx~CR>zqs8uT8fYtuDfTjAkF;~&a!C2q=Kfv{SHK)S79I|$av^kdGp~g@L zzJdmU4*U?;kP4TJUhtfUr`{YQzLZ%iv;3`)*W;wBw}2=PUvJDU(t`7xOoYtHY)Gc} zBd6ze(-SAzwvX3qIDK)PYFuw*IPVE1?M{BUWGi)wdYE?HN~Y6serq`)tK;}a_uHk&+ezgY@585tT8ZJ_Bls&@g0+MbZau* zColAT`L;7LDh|L0vtSRYA+xb;^k0%+nYBiMz^opPF~CHiqq5(slEOUonljzWqK*Ib zH)xjMUP!4}H;F2MZYt5Xk^BXJY1V7H3wkD^FkP?cRXVUq>o-EDHNL+fAB^Hp6=t>K zV;IrAl-RNCxHeyF5xz%h`n*s4gZGa-wjkf-k1lvngxzchdmMuNdB`=fSD?W+eB!Yt zvL7)!^r}@D{I-d#v_nRTQaU@^Sn+C{Oz;Duft(-wj{^>M=t1*#Uq%64;0O zOWIV;rh$V-G=g-IB4&!Ce}&N$l{_J2D{dLkGvVOMKjt;YB9J^B9Y0O*5`HVZ838$t zj%Op@7Ipw`0oAV!vEdlP3}&XS8hko(lq&Gruu60YRM1D%rR)ZEer6FBzb{=HX$F&M zaA*@|y@X~z@aA7oES;Lia`2}`q8#z;O>*<*isx?%1H-xqc4Kj$d!X6w8#)E2(?rn zlxtKgpMh*9cjkOqgO*`(W;BnW+Uef);m+QV(m8=a%axOhS&z!zk5S06HAy!~gUl{y zbL+X(&UT%Azde59%L7=y6$eC2u$LN&lkFWG1Fp%D$%RtEo`)FJXE8*A3||SKAitBs zmI0(8Cv~$!p`c)_*g8t5uYoN|ylDvME*ZVI?~LvB=tLipq^bodLE%6eS!LIW-cMrJ zK{3NUIz1yq7GU3Uum3oG)^>edR9T^?HF{><+w*bpMt?r^Mi;QXMAS+E>aY~%b z@T5AkK1zm$>kB++O4?x z$4cmiHorJ5d!r%ErR?lFC>`KO6(G)1^tZEn7FJePcq6SGqCgQ4@A3n3!dg<{82*0h zSAs+i&Otqvx|0V%8JvRT1u_ibk!+-uQvUJ{Z9It~3{Xcsj>NiL`n7)Kx>M+NN@+B; zzJy|;0A^UGZ6-+b`@Ls<%PJ0BmFNBg$sn^ODhQJGlW9=cs^6^WE;tT<{sd0=kG@+k z0#WTK)3rcZRiR+iZvt6`X?=APn0`~COKaiQNk(G9qz>)WZ=YI}7&N~DL64>*0wKL~$vI(#E zf&8Sup5byg+AQV2SU431LY~D2MxMoX`o7Fi9my9}vU_2Y%sMfr7a9?ua>GNS(#mkb zu&r6bjw3T0vk1I3SRUur#j?B3p5*;MJj75VUX?a`1ln}g2Pm@xs){TU9RcmsAg!*e zErZuiwODYmKEgzAl-Sh%c+YHYBCQos8A}`5!r+Xc{CfQ@S5bLoo_t2?113_7p$RXf zltkN`#ByF$X<%zi;ApL24B^MTV2k2<0Xe6W{qtF#q}4`DTfp!PY*4f&_mnYRagPzP z?8LaTb#xh=vCz^Ssu8Q`QN_ZhU<)?ecE9S#vhRBnneq8Db-pv2oPa zArDG_UHwf~D;4~f&{gQ3Oc9-srhoj6jjOy#ONoBfG>VyAM+XL^@iXp*W{IU?l4f`H zTbboHWBXM>gM=y`%@SABo>!FOn^4wXA$XuD5I)YS?(7}LQsFOwaX=MHWnIE0JKpv} z`Y}BqxLYdcAD8T*&g@Elbp?@iK%S?5{f#wNN5Xo=R7pDg=cZ~YpD-0vR^EPU4_-o5 zRRh#1L{bOfoc5LdDypmB-zA_?&}J9VTQeydJ!KFdzUGm#m$2|$=H!6-9<5N1yF--T*^;~wrs#-;49X6 zerqqR;~dc*T}IdDCQa!VyVt3)PYYH}xvM-q*Py8288Roglv6*e#MqaZEbJdD=^lvN z<0#5&?U32MmZ;f0EuYA`UV+1$skR1HLh#xNR^c6%p6xhExcs!GHco$6Y;lt^{rt#5)BF^22*?1=r#}G-q~Ep0R^YV}zJJs6qMgO8#c|}e zC6J=RpzwM_7`HyvpxYgpo+c*Sj(z5Ryt=!HqG&2HXXGXms*US1haiP+`}ZVPvcF~$ zej`91Ud76`p4u(5PAAc6zl93eimR5h3bk*YFqz?8$5$3rI}M#ao8Z6W;t3CA4`uN_ z8QaBVq_-!xKwnlLuan~0pKGK>n_k06A6sl^irJHS-De1H#9&XC{3Nr%=Gyz&8jfH7 zXWI-w`lrYNhi2(BLXyXRjp>06Qi%Okl(%H&=nQXz6;~<^euNeP#tC%Q*LUMVN_`4c zNS~r|mT%_A%rXRP?3~!VAm_c)deQGvlk^%rSz-nH6XsIDbEB@q>4d``SzWBEuR znNbY4DE!>E7ykw3BC&Bl&?4rs4h+&oLf1^>Qbc_acqp|HZ2O&Ft@tb~(_P4F-TIaf0q& zI2|&_F||`6yXRoxtl0;oEw+P5VSzps+YOJ_*HTWl#Qe#uwQLuVcX%7#GpZeIKaOTR zU%gfY$)?@o<+JLPLYrCv`>Q`ly`F^bZI42?qP-hmh?iArFxX!ZgS4|E`gjx}I3w-y z$3>2**pd0@j0p!W-I5AwYpW?;tI=ugd;I0pdcXYrN@^ksnI5@K)aKoIL+3%nkoZWF zmYm+CeYQrq3%s4d$%mvbR`tL~j9DA_2=wNcru1Azb0s}A8>x42W|B-F5G_VJBc&_X zz-s9^syI)W&13$tkL9XP(`34nmScZKm7Tui$kV7U)nedIwv}3l0U@^cR81IS$-BJ# z>u18YK+$lo&I9I2FK%#Nuwt(MLNEFqq_x&Spr7~#^Gr#u?QU~07ih7bIXODHLAyL9 zN|7JNgGH;Q9;zeg)h_Qqvkh19W35#6Wzytnkg-mF{lkYRUX`cIhbM+m`NSb7-@uks z&z}HG8ORYTMU?rp?J5i1DC@44uz3hpRhvfQEaf$7BA9G!eW23pb)mS)*X|81s$fgx zT@}^`@uW}|CO1J2`l6i;>|;rn$)uok=xIDg5vfxVounjjQD6r}TL!1<<7YVQl+S&r z^7xVEgTQgs{^a*IiP>oUV>hE|^nHWEg|XNgc-oT5 zS(L>OvYbCZ24H4X>H+|ZD3VbF!KR2FW}ZrLpgT@el&YX-5(0^&pS+nfg#Bsxr@KMM zkGAk{!%ZMAO5SK+qXE?KM)OdB*!dg0XDyue@ZqCj5hx4J>7xfz;YMZMi}`X-^{b-~ zG?~%rYvRyL3l0xKJ8=bJ`g)V#+qoRpd}RutL?Pzz?`$AR1ojP2qQs|y3i%zIVJ{cy zatjLGsexJYMHtK!aJU(InKa74!IJ7ZT^437y7*16aGZi=#Zbhm5GuCBm$ad$d%R!z z7C@q|T22*tMbhd+tN1y{5eJYzXi`^^=kzAB313F+{h#6F?8xFEgwEg)8nyxcI_uSj zDaKac)s_7Bdd&Y^lOyr}de)_Fjf}1SMZvC$m;df6e|O_NRXra()XbESQ6iQKk&Rhq z(IUE$#ibxg1>LrBB|9eTMVnngbY6av=7z!f{NPKpfAok0^PaPDaAf+Lcy}>%`St$( zh{Kz`Zyw5+Ox5c5HpLBo3r_`r3t|SWW}KP;$Es63zvk)a94}Yg}#+?7*qCe*_g=^-HLmE)Yl1(jbK` zN&b{8IAV#~_iOfe4M3Qx`QdyQ~boRgJxS*%9ln110oWFKCUD}c_`sT9^#IDAeq@* z)a0HVG?Wcebr8l)%tu_LV)|6m0|k=_XO=Qq_U&yMAxD$K9gRCNW7btd&OCbgmAgxb z5;+`(ibfZog_eUokJ$_r-FzdETWQDEDBP~fX44zj-ATrXbhzcgrQF4Q&(bI4l1`8R zvX2YMmt%G-{}MqTry6k&gEYy`CAKnE@025!$H8`=f}c;q2>(Ji@fpcG9l5rnkSlz1 z7^P=MLrG?-o@Z;ux@T+BF{1^oZqGlY?zPAzg)`qdW9M(R>;Jt#7W;3((ciVP8jKzC zA%;))#@6LwJp;jR?*Q>T za=EgGwUdRWIZ3YC!fQa&(x+vGb=4xqx{N93Wn-4PDW&(;=Hw9~kW+lwsB6b|_x2m_ z_5J7N-J0k$b`TuPGk&1stY&E23gtK}&U!#ADKO0P?+<5AUZnm%tptJiHqiFR4fPk` zdB`~lU|fgq(wsKQS(*n&Ds5{dsx4!A(;>B3Eoi&28x(L?kh@T=T!EgM6`qHEXd*T- z++DKCSciij`Fb@gYh>doM(+H$+~kD1$?GA4Zs>2uFX0yBYg<*VO!d~mB;JaVN->UQC3k*j`T$~eu^TKcujDW|3MO%u-)Y?sWU0lX3h2Xgi~m?^o^(`rwxl#=QRJNL8_qmBjMi`m#dw#zbhHo zNZS;bGHuurQqs}_H##yjs;T0X)wnqeBsZc^)1~I1XpGlw&;y_jHX=td=Cdj)ADu|6 z=|6x2-3XhPLBxX%f0q%5)vixCM`7+t;1(nHw2CiLR@Vl!SFv`m#GO@68dF!ez@RN7 z=*~$nm?kr|Z<-8G0Vc_jUpG2b%$;g=uR}uRHw+JIKs3QFwi2EdYRwThFSbDwutrXP zueRZjKQ(MeX^vuawbmE`l^&0k2icU+$gnO(i)Jjyx^HgK4m08=BFH>pUr&%EwoEDX zVAaG>v`7)$1rUiEi!>fqho|;73Fb=~6?X?A-b~Lx9ww`Q*^;<@(kQ5L31M(4@IAGl zG+1PI-q-lMffY2Z6 z*-Pw9mA3SE zX@%Wgnu$4r7xoIYkdFnu>f?<>8s2WKS?f0*7k_)%d%5%Cdl>fZtB zu-p`(jbfK>ll^q}R6)0rjX_i}-N4L)Mj?c#q2Dz|Y@y##uN?xTz5S_)qfv2jS{9xn;+kj6dlGbv`V#WQIqNQ}-C4y9WzD&Oj(dNKcX7V3 z)0yh3n{d8nbgt%#TyCim*8oe)|o^Qk0fZS?F z?PFiBuIVvhv!7wHz-ee2t0aIt$XNR5 z(;kN8iwEc6DgZHB>|iQ(|oc@`cOUyqgxS z)@IXFbMh-Z6ez|ZDf^UsBD0}x?llr#rl_QUc7BYDPi`y71bQ&rb;Y`(0+R1sFR1+? zC{1;N2VroA?_NPLv#zu6=1YEoBd~*i1%O1+16YQ*aqRLvrw)V{f+%MQDo0zJwlZ!7 zM+16PCPJM}6huPhB3)$nhq_bW1NXOH{2=EJl#{e_;R|0HvuIp;C}4j?hI}-@z{_by zi(;@zTCha8G|SShn17Q)nC*YvIF{GI3k7cUiHr~d{2*U1tEk0&w7mTo*{;}iX~U+* z3ha*66R-o_O+r{Z1y&U>WAM)iATc^a3Var)7Ge5o)I+(h*}JhBk#M?|(b78febSP; z*-%KUZ$(Ve!=TBMF}GhA%{n&Za2A6;Qm|zb*9E4gRZh2IFBXgUS- zuk(p436b?U0ebw@zzW(OZ6!z6O;c$*rYq9M`z2}R0oKX_x)^wY-joM_Zmq0{Z9kod zSUHSINmVE?LLlF>6s5y-9YMZ$-fx+%%@9}V zitkfyG8e6i{MaJiKk9*eshL$MQ5{z-Xdhp{8-6-Hx|Cs18Cs|Rj#QNJe^t9ESB z+gTvvK7y!y#8$s>=8%oJNbg<~8A2r7%7@I%xlb%lNo7$d(bAz+4R&3M4{Pa>5FIw& zW*dp?gYv@lq?@ObXZN-%y9s7Pn9qjvO7rB4*25las`v8YQRdDYji)Q4$O$gtLx-_7 z#A-v7?wXA2&FvGw;S-;eq5&jkM}u2p$@O-6pY>^(z1wr+`muHU)V^n6{}a?J{lX&h z)!7C4eXwpe+TZS15e`UJ>e>Z4aF|&%PLQBQj=3m-uv3x?a=6*h!<{EslLYDoM)LOZ zm+sx$1QEG)!y%l$gEu`Y#FE@sqR*q@EL)W6-Q8Dk=ez5keQfui`IG^pJ#&-2hKarc zqaBXdPj1I+5R=V_iI$Ynx$%iF!-ps3y9aC@pRAWJP{%Ln`WIB9H&&aXY|mR89qvcB z+m-#HeONl&jxM*0JBz&aiXdqn8mCX$rq}_uVYzHd){WNnsW}_23Stmt6gVL+IRQ?? zMd;xNUgY6&?~hc5m0hw~{2SgRFaAS3#5Fvzg`Abcugv8ON}d+VO#l*AQ~`;G^P5P% zVNVPfXJ#ML0=BN<2)2;8A0FT&2vzxbrb7qamBTPs>l4{&0i{!fs5^8Ee^d$Iu(Ut9 z5IzDDc5*@;Y+*k|*1wt&rW~m~z|Xk@ooNO6!Y+uLc+b*?L7PJdsb1inD z`f1IpI5bMko&9(xN+G~5(oRz%9c$lkE_^Vu0Z?EAo4!vp1|;wB%8xICIy{0xFLGr} z!rw9Gl)j~N_N-5|as*nX;Z|VD9K$?dsBW;-b~xZOC>RmEfg5#VQK>ej2{{Cg!}&&2 zq%IaCWh9BoZOYjD6@E3Z^!Z+r7{KOmEXsgFNk?6w$e#G&tF6-|8zB&jKpgY^99lF3t(9xi32j3RQSIRVsZyxR^A4$WOxiBeluO&FXycIel%eAlF=N;~{;Sq5g4lvO zSRB$yx|CWY)5p*V6eh$fVLB00DC*s+N9AmT(3>uqExxZUtj-_ zd&b?6ucknKdq{n6xB1VDO7Z{KVnE5k*!X|fl>c&IUh=7=0tW}AgzPI!?iKWxg^Xh+ zAt>Vp-}PS6StB@?V&Xz}lm9}5z=MGM{P8RCcB)+fPb$<;`e;9!*eRSRvcdJotonI3@9TG@ zci_`EYP2>;yAZGN$_gOCll}p?o4jO{5hkT*Q0VN+bx(!uGI(o~K~tp3NpfCFbkn}Y zn+W8NSTrKxN`x8f5_kPP+FAI$FFjl@z_CRU#2?4e$Z$=GcW$AlliEEAo@FACyg;FE z9KMErK-B||0c7b@@^JX;r|2V&x}U-3@n&lSnjZmgO8fm7`tcWM2=#5jBO3Qvz`aJ$ z7bLd@c&pE7hjTR#B6Br9CKMWZrg3KMF{7g|o!j93pe<@JeS8^kTd^wMcoXl;$K#%|2afN z|2rV^&vLO!<)0RXs0OIZDD>ceia=<{3Gy|mnw!XxfymL}37URtUdC(n?~!7$VSzU< zEp_)iS-xSt_DwAq6;C{XJ%~DcOif}63h)v4PfTt(PJ2%|>O5wC9B=o0{$!HbLxcH9 z-HO_8?U$4)Fb+0upsa4zlM#YnR{EYQM1draxVx(k{Xy+q_Q#?TGiwdgq}*DD@2Af3 z=^eL5ab#p=7?H*@nUbP~8gQ8tq6+SET2gBoOt*_FJPm8yuZ}R7a?wH*_U;R$OVv-* z2YI`0joy!1t)^G>GBZ&mEaGnyFP_p=AIFlnqj)E@g3(fi51lLUZ%$1@X$YFgx7$C; zmyVa45Kn(}o^Nuyh638E7oio!YQS|3!WVUYvA2X}ibH+$P>IK=hRdcyvhvh$(|lK= z$G~-CXfr3dXx(42r$R#$k@Z7}u1c*a)~|LCRzp#rPPK=0XXuL=07_z7pD2z3nQp2# z_m>;(8<=h@%oRsQaa!8QA}1A@YRII<0c{0j*YN-C|GhB)I(n%SPOw!VuAVz&XZS26 zBNiT;lwjPYJAIaa>LEGjRh#+F5*ccsfRVlx_2?wl#yQlgdk3QeE?p0`Zc|~)ux06 z$aDX+-$td7<_%$g# z#?QE__;Y@n1YtEzdUY!x@RdHxkH|i^ZAmR?$ko;?)t!b{l6BsHZ!{j{8d&Fo85cdn zMygNvU7U%Qy)D$yOdr9SmYGoGBa-L{LT91*z#3ct;bq9k-$nXTd5A~DqPB@=6X4B+wv`YUwN_# zw!OLbqVlkfMW=Y!0t&Og>g8)P9~M)lvaTsWyVUjZWxw$k)_{GT5vrAKF@D0Z7ge%S zJs;Vdm#f>((p2_KuJ*1bS4x0N8n&!{HWQtiXMNu^zB)Fv(}TK<$~Mz0Ez`F#7puu3 zPu$^eXj>BXwBz-MQOM}I6k|SLQeZdXSw-Jtl#lX-vohN`k8l?@v4Sw4CsCQiJmK`5 zTndLhh;$+n;Lq467>$nFh^6_)N5KJ%MtyJ`_K4+!v`*QQ8lbyV=vBqSB!X>;xXOkl zy~0TCxlvI6uvm?slGOEnn-GlEb&I1pX0-a#D(9Pn80e&tIYW7Yonb9rsKFZn=&?y7%FnsTI&t(KB7Gt9ZU(O@`-PiQJ(yq;zqpYs zys4c79(tBV=ZM2BiPdAxL@C+I^JYiG?wzQ_s4kTk{#yK${eT*qP(^nG1^+AjQ9{>@uDCdx#g`?x|MpF!lgqpgPpIPQIcEP8txi zEF5K_T!0yqe^#Next%!0$i5-NhSk@*XZza6qUR|?m$TqhiqGkyWa?r11aI#2`YQMb zpp0nNM0VG4w&V0m)(Fq)w=eAnTwpC6pKu+}@p>%|8u6G2ga0h~u$_FoWPD_c{GosF z_?fDV4^72i&lAE-RZp8IP>R}$5bc~ZA;=%2;}Z`f5jN=GXHONYngh`ct4^$9T7F_P;)NrX}YP#DU)OW z<8ZMl1&)*qg&er$Ii~{%%@wwC>5@Y6)+7IP>eHNHd68(QhVnC+Q6$O$6%8fLF!{dB zI6IvJd6HERbc~!L<|*T)l=y*dwhQW1+Ey!hdOe3LDee#1;Bpx*mX6+f7dnb)CYb?R zjX5jNZha>!lVZwF6b~U2^%doL+vllH{c$h0ajQ)OLzFyJ& zgR-)+N~@XCO#Q{dJsi!ve0&R)fbeiYI*xmT8yh=}=0Ju5`~%MWO2y>xl&tg&w1MJ_ zJ)Cpq1D{iY5t~Hki+)AU>_HirW>_#3n3{7t`C0j-K_L+Vug%UgG5oTEU=E!7)cM-n zeblX)o8st}n`Zw4IBipy>k~J$>r*#8Q)e$kVfw0=N@wQ2XpF?VMu5Rn@Yk9-*qusLk?wm@Mk-Dw}gvc zVMZ)^RTL|oB%u6au_<5uktCqy`{jtdQMtzFw0|j7i#$2@jgS7yTfm?PN?DPyi zTZm7Ay`Sy*ar0(Er&rhB7a>qf+?VE)a)@kjQ^#i&1(!{8=IV-5$5YL85;rp9X17mzSst5Z)9Bi!2Q)0ecS> zY_uPY;g-q*b_K&YL>TsoOjBy}%vUGOI5x63j3BXqlNemH($on-r};tJmQfin6qy{> zUSG*$AZ=7<^4XE`qKfqb*APvH)lHQ+n!2ELLff9A!F0y9z|BKT9ie5trkZhCQjaC` zwnpihAM#EKi=)=8)wjTV3qEp zG4Vd!I2+++Au~RS)m?Tel*$~%qPalzbDrM8u zj**eefUx>c62667NmwxttL?2;{hT8SpVkyabJs*Bj;|J9VA8ForUQ5MV6aTIR;G&R z7l~t=@DKU}5!U>Cp6$88oZLVnO_!QaEx+z1-ZZSNxoFuf-90TDjT@IB zLv-@QqAfM8PP$498z(hEYzh;EK;9A-xHvPg;mjXameTY<*;|H%FL=-Zjh9@VYvflsn}Vx$;jvh1$?ew5Z==}Goh#X~ z7;cJeGBr~Oo1hLWY8wofrn)m!*EO!4k+kIuAg_ssAfFY{K>nFk32Lreoyy@_p;lBU z2vCtr%_T*_%v+B#qBOdFo&EA=L+(^~L%}p5WW`ZB0iOmn7t4dpFk~*DId_uC6W1BN zr5-;8B?=sUut#FwywhL0f3=_pcoZQQEx>fl^} zvV~WmHB2Lz%XjY|*G^#&AP3lqt1|;&T@;J~t2m{uU_7X}YeDv40+c*z5Ycswi2lM0 ziKVDw;URI4?^WF(`YSY`u+@{BIBao$DCZtF_ClJkI2f$=bcn!0E8$s$m@|3>X+f}N z@mAusGd@wkJaq)1*43T~w}yP68bmCsnh52OxWMGfP~Ww|L8GT#sqhtnGt8&dBq9%q zv4dE(!nj;gyj_#T!{YX@rB<7wpDPgj*+AR(iT*bo_COxDT^Yq$h3ndSbmqu7=y?NT z*@9#`el>nN&QrvtlkllwyTv-yqTxRmyfI!;AE%@~$+=#-jLyHvI!x8&b3L?X>;t1TL=Y{h| z*|go3_)b_>omRw95N;ows})V<7Sc%UoJ#F=RrWQ#+NtNrbj1=0&{pIv;@-DO8M4(l zSN}pWn3|U}TsTC&k=gzjuEMJi4NTV0A|Qp;z4H6g=XM9=Rro=Rhz|MGVOhR4^m^KeQMB&ayyjC-PL}+ z?#AF=O>riZbW>ye!VeA;HMt$t-+RIAgQNc<73OCA+6Y!shjn;iE&R>+rRUk_i2gD` zLBEoT^`3Pg>ZMt+fTT1fv~qo8;033_Cx=zFs7!s>Ljx%-5G!@!kMwo)4nFULq zUccm~O^C}`{rrUt#XB`pW!T@{!@2OVL9q6*sR^DZ%c%{n`}^f5yik3BX{prKG=4xP zh;NZ2&&KRPWf=2SdTYoVDPt;AEDOEu4!}UEML2!eU@YBuu0EIbh_%CdLR3=n2OUPtsE3i(Ilui^FUS_(eSUI)!$hr$)-XSmgawB@LUe}{HE;0`pGzYfixHXKi)0>!)O&`f zyr-s^L7#t61Sl%aidubbWC$v*{!nju(1;q-&0xP`CDy3=N-t)TtJxa#{_GLYi0#U? z15MNKOz8Tls%UuyITp z&TNB!h({)Pg{fo^TMkb60F6)m8X8R*B^B8xU-e!4VvJIasj>q>l1Fa9OW4J%elqm_ zc3sviY7j-|5WH{;$CEwwK)>vp5rjMD%QnIwqSM1+iyffWu1~N! zg%W;8jKl5u2jvPkcL&z|n_AiVx1i zu}B#ep>q+xDc4*96A@ZRXQCNrjbMDr#0Bzh@9V}iRZrUM<}Y^w(`IncNI0j2woKcp zLoUauscGHqU-(g;ZWaT>i`4b#E%q=->RKZ(SZr}@^E4TQ>#SG3P4wn@z4V1#gj2Fi ziIxgYJbZT=4(K%sqC+@)_u6ol6me6UCW6(q_2zMT?4SkE2JqYme6=TJCl`j(L?r;;S-}v59 z8i=is*co9UD`X^3dxs`Dv-)i_p^8jaLKAG?(!6W?lHF6q(>)|~Nt1C;(jYy`l+XN# zhXd-V?4q2|gB<)ZRG?oHSr?#>jFahaxLYlBm|efakvi-R#RQA1_q(q$?dWEvp9n1G zHjt9bZdmwJcVnD}Z%&J$P&?t^gdocT-Pt)FPg^bH+;|o7+`odIp_GvuFBaW-0Ia-% z714Gv;gcMvr@N?t0y{BOBV564U3W{qVg#qHi?G0Eu$G=bB&A&0(X}S}Fn1%g=`N$a zKS1wG!hoiL&lh}s%(!O+m5< zrk5i;g=jK=8hTGIn^jzPMNvykbR+YVgtAN0fPjS-x%DRB5<-<#Z-jWNyLaPRT4f826SzDJ?Kl31rOL{iy)=mKMD zres&*-+{7$l$tSGn0CesQR&)*A?!fCPaqP8+lehQwp(GAZze$+UR31AO7>u%X*S?D zMO`~EbPl-qk+1+eR<4$76+&sri?;Iq>%c`<-5OoXr%J(?4VbsdVctRN`Gh}r;Z`t6 zJ85p}Rz79|Y*mt7;rdLC6l{+Cc-o(&#Qz7dzpJ@Kal$AyQMA+D)T~!udmv-8M2V*n zY!gR*cdteTS3DBN*tZM2U|AHa_wVM0k01UmO?NfKerw<PEYsHi3tHo_vmr07tXX zb{GP_97zX}$j2x`gurjq6zQRv;>0?T6LBsq1P1hmtd{q^FJmBUa^P zpCWM+>cZ=3NYaRE#<_aWa1G_P0b?wG88 z1)1^j3gvwqKo3px+WV=702^o>HrZ$Y?zO~4nd+T#fPu22({~TvGT3H;S}T#_5xIqu z=xJm5s-6bEyC}NffO7FYZ|y4tw0TX}RC`eNA9+~*mgs?j*pgfH<2Lx=+;U=lNUVIzLgZ$GFt_W8j$OVvl# zBC;)DH;_V9Vp#4Ua6zfsf*ii=3(1{aOmCBHTuf^(pZn)*Mz!HhfL7E8aRbs_)JOJE zEl_IxI9M!LuGSk!?0iWAG?Br?GiN_uoAF@3;u**UGO@U@Zu<3cqq)0hsT^; z`oIranN35lUu+GGhusme|!6At{; z#b0L)qICXt8 zbip}URsj@^y!!=a-M|vR*1b8|*#vZ;f5CZvKvA|afh5Z^CuJBL$ z?D0$Neq5gr04imB9ks6SCuRF`N&3()1d^q3YU{e}-L8dqz*#L`zeOz$fujTfojsNj z{vDnKUmh~sdM5!t`M=$n$jjQ$<%=>Er}-QONUMM{!?MZ3=>SO$pV zZ!mIe{3ha$=biA&_U9*o0i>W26X38Dz%or|cewn$Rs1lB2T1%E7TcYxGeofzVOOKZk2&{Hr~KCNqkiKNX90V|-cEJrLMgb3951GC~6MxvTq(}ifM`4Sner9bR50h1cqh#(3SL8yf(hk(Y zBD?#H*rA#XO7kwz;AF&;69wyCTXe@6(t##z`1sKA$t!*5+I|*39W~KHz zx`A#fz^ds;4s8$s@Yd;q(0CW}`NWmEsdQ26taF9lDP!1 z-vtyRzt`zwqm!ip{cDi*-ZyU+|xpmbC!0P<(}%T z4neB4-KH@ilV^AaGh({afg;@{_3rW2Y{4jqXz2lTR5y5KGPL$?WNg>cGzSA_lvt{p zn$S~gZIG?11D8{jNlSQ#2Qw=0K#nQ--A~={Yt&$QXT!PUVsInt<>h0I-*KGOJO ztG`d@4SJ*%(JWbSNlwziPaYboO(q$x)(M<@t-A8LzT7$8X6`DAAdP=)gMD|IbC{m1$qYcKHz~jCs(r) zb(66Rxr4E7BS4><9k?4&jB98ZUNbK{vei}3>kZGVZ{G|)pKNbYkeFHX+~6QiTK!p> z7A!Tv@ev$6S8r-2sVMg)6?aN>m3=KZLZ9A*ywG~D8!KdE&*_3XWKluUZxhbgbHm$R z{Baf#kAz-Slp4kb*XSe+NFt=*bd2BmABp-OUj%H5<_^-mO(_cE4Z}BeikMZv17ID{ zi`ht&kwu7_A#dJ!+xI>{<4iKixYrLgG=3pgYM`L^!gtC`oahWk2+9?zCX2p-oFd*0 zj%sxwdC)Dt5gMzV4HEGlA@DKAp+gzxJEzo}TZ{V6Hluzf4bH5L!8sG^NJ{E|PH)Lt zIs2M70%`q(*MoG%qXPI=#^AV_n?N(!{K8NTFzet$4AWP8KTkGPM;GX4RN_qDtcah) zFfmW6a1ufelcP`#ma(iFY$BSS%{NsoklYy%nlBe|0dirk@<_IS#Xt`_L~wk~*QH@~>iudF&0T8#Bi0>9CyFu-4#*Ykfo^RoR5 zVA8fo4DjAEg3ap9RuD6HvvWTH1fSJl@nJb1TW8N^Vr?$NkVRy=qr~z2r|#p`%}BjYq+W ztWpB4%$el=NI$soBWUwfyd2_JIa{3IlDzL-Nv8oA^6G+4fkUgbl`LU(efu2bKIw@e ze4(BFU=@mW{$*Zwpri*N_#{ED&w(-P$R95wk2`HzBsrx>+3Z0$Rm0+0Fk82E5%U&A z`qf8bK#(N8;x!S{$PFlBNd1p89Di%&lLUpfxUlZd^j5TnV}L3HVTK@rE>Od76X9l$ z2FFd`1lrN3C#sVFp@S8%+*s}7a!fm2^A})Anlamv0n->XMI4iFY-pDRvcVUQ#^+rs zf3&y~a9}>}E1Y0}lH^=H;&BG5I}-1fDCZd?9gjwhtP^xgT(|y#tn~OsDSYW!vPSR-BP#>yWrpFSsITqp>;yAYKYXs)I)i&sE+i*7b zD91pwEs8QoX+T-m5eCg0g)qbC8|XiNVv-JuX1Bi>^Zx%GV{HG0F@^ua*aD5ZfWV~a zyP3H^wg7w@u$Wk00Y#HSu`<8K`S!XLl4z#$v9rR5A)BMff#5+O+#c)~Dox*-kRQlF zLbBCtSGs#n>tXEfjxQfZ>s%ClXr!`|9r=hcm8e>{9q~X#Ul-O{fxD<4A$<$&a=rvO z5#wSnXVQ8_$ z0}H#ZyD!VkLu!@y1K|*=mTe)ITwUqHkXf+R#uT@cq}%w#GJ40k+aw)!LqAG+TMq-c z({Am30mbp@bB$hFaA|rU7s3zNEcnXxhu?RF#R9n+2=eKm}yDe~-yF zC_+`Lzl1=GH2;SAK|$^8`A7JXIk@}#L44`sL>p9VW->~V}P=8!EV1nz4lCW6MnsC1Z5hx*+)j%?qwqO5Ell4w;r%Swm!z# zKAyssa8MBX8zSB~9nxSR*|!GaV<@+Ymf-Bh+to&4V!Ir6Ztaj7a^0Nn05kN2-b%jt zL&MSXj$I3OeF?Jq3|$Y}a2<@qwtpg(yp?0|9`?^}zk#;)8byc_bCDSGz&6_T8p!)h zxApF~?>?N`KETMJ(e`Ny=^H;kvTaKI zr98rM!$Z37j&Zq;4A~_YfOe1A_<{BMZ>HOK?2Yu_a3$@e8|cqTyQ*U{Mp%3G*2(4GVpf>Z`CXH&N198%e!l66aVxbxRT=K zVs!ULj+t8N_dgX=gFxJ+jX-=wRu;~x4A2RW0gI_4bLufHkPO*uPsT}cBmX}uXPzJx zoUY}a%iRCg^vD)Yb$2hQ=4$S_#d)-aP*Ke|nde+g2Mz?hB0Z!832{hZMRz0;n>4z( zkRj74L08;Ifogm@{VJCm)YVPN3arE6u9l*zFJ|bbm7FdBnY*-^1-M*Dv`u`;Nah$c ze(FnrTlPzy%t84)l_QLO*E#= z;gbqFkQ&k?rh;*64mTXmEW{$@31VKtzfE6l)XktT-WWaCVGz+slL)s_k#!oy4Lj)RVK>wcs@$Cq{A)IZ94s#J>-rxMJ}OQEglV&4PNtG!F*|%B z`MfZJeN5o6-~=)SA^Q5E^wKHG{?EC>zYx+0HuN%x4FnQ(v50&-dBk|dkFb>Dk8E&?;yp|M+NjXg*lUxz#v=hkm{7IAr0!fkv1q3AF5*#S* z4`|}KMgvL81sVv6!qI)^)gD7ig7Imh%-l|t!%}eC_BlGsN`pBv-L{RWbJ2TT!czO& z(dFA1rpt`z44>Q0OMg!wg}J=OJrhjs=ceK3)zZq@#yX^s(ipvbF$u+w$#`(U zK~b~A+{q9jMgxrorTEjGzyg<{VhwM$yeh*u=oJwfhpa$V_bpnMv4yij4xxCW;mF$p zcvT-vd3|}r@a#M8pxmy$uFN4rxzD|h%%#P7!0sE=^hoY~Ui~w~@Hl^ZE9oS~a*w

6UwPsnTBj%i3M75K<@#bl!;O^TI@~6z#Zat=}w2K1{0)2FtRIBDGD*#YBNjd%Ra-JuVXO6Fvs01G~29P zzhtjxRM+_TJf%thTN;#kb|8MEv> zf8WFF89dO4|6D;!(T;LXfaqy5CYBKMp z=$`95C{fm$Ptxv4Nub&^$k1mj#v0yC_g+_H(B<+tje5w}tr983>>a>-3{PV;){Xy4 zF|Z7svXfaqid%qHZ2f%d!!&u1mX8UPwh%fDW#WG5Y*LfDUQ8ICbg<=lJnf)BTk<$V zQ+P8{-@-2|EvT!~jjle9Tx_4H0v3KmN_edP&bN!p=}hVq6YfmfS-adC)Swe)S>Ch< zdYl@|a~4~{{NT%QXWo=0?W)p#+G26O#OUW(%1x~`NHEA~CvJ+J)UVaviXybhRQinP z+2$OeTzl6y>TwOC&_BB1$4|7GFQDMwW(RFwGtY8ZA5b=tNy?BOnow1rhHx<#{Rn{EKRgWgtfd92MxD&3BB>m=tf0?l4Z6(GeO748 z8_gs92)b1BgcEoNldm^QkBl2UdD;nF0}>Hau(oQ4j0h7My0^o>IYRvR@d94Lt&B*{ z1IwO_FRX|0@4PF5y>=7>lRi{R9;zfgTxA{5WnH97rInDDAD!Man3w>+FTz=jmTI51 zeZ@2LPUyX;{D^{--df1Wd2-D}!Gr-~?bizOog4CkD^h%Rw1QQ=-%Nvgk)H(uF%m@O zD=GS(0-GSalpm7|cRSvXXict!&V#R3De=HoVMi@~jZ zo%UCi%b8~w#!w}mt5fI&IyskoOB>?fJ^lXEDKZmVvtiDp(2fN2_-0wS@Y&AXHK}q zV_huj-atH|RLx6zw9F^!8J4uulWG!uxh&75*5pO^Shv^dw)1Ygzo*pBfq zdxkw91#0UXVDe=+wu(-2A=-iBly=^bSWXeQND*w-K*2QG11hzX3j5^44u^VP|6rn( zf>1g6LxOQs(m5v?nBY4^%!L7rm$XjF#*`T;TIxv+3zw8LH7UYJ8d*;}jA2-@D*HC* z#Il*ph)McTp%?k$RiTOM5`{V()kXjIkcZLcBucg6%dx+?%}Y8~mGKImbMl#2ST?f7*5c*U&^{7$T zGQ9Le!{(Px{VyGK51P=cYk3wV- z*N7h%{-GAiiz#Sryo8{@ycJ0On?GuMH$T74Hy8~Ry5mESGoeUU3nUwdlT)Plima_C zo9H2%U@@y!$CIfqG0Y)4Oz5-88r5fj!IinH_@) z%H$h1!#+BN>6K$d`xln-tPJ-rbqS{{VD9&&Vu^JrN=)#=QFSA;zqR^mB$*!p6eCb6 zlX1(!(z64wjgwl9iyDRCT2CNbjwAG`$#z!(TVQ6wK}k{Dwvzy$tI;2_Kwn<&ct6m$ z0^vtuuj~zGWF(ulABn7%33F8r>&V3x_G^w(ZClo#|12D&W9V+W{q-y*;{A8b!~g4q zzyFnaP%ySJGXBjG{GTV(Y6VNn|4<$*ZgXK(w}@LVX=v(-dspF6gdxbIQO3*lr}#hw zNU^kzkDSY_S>gVJ@f8Qn-vjsc_K z;*;CoLOPUM)TMZpMD%<`WmFzKHp^dD5DG_DWNBfa6w^5wHr?lEjw5bxVlOyjhqMJ^I^ATT6*3KN9#tc@XMmV{sILQtR$B}TdN4HLO z>S!0X$c$3HLBt6okR)Re2qJL#NBy>92GBV!xyTYSyt7UN;W)~}f(j8_TWCJ+gHH16 zMnh|LH;=2R8_@fD@l>OFtMtxjst1uARq{{OxltrzV`m4RN7Hp?OKtXcKe))>P$C8P ztig9|oBfu)%bzD-5S_2a1>5K;aN7&_W9U(}(COJFFVvv>LHcRRP}6=#gGcm(KY{E< zY$S#b80s?;Xo$f^_|8%7q~``7W);cD7;a&aYZ1>o zv9nT7&>DTgZ!uh_R3q!|RQGiTN_+aFX6d2?B9v`I%$Q0?@du8mG>1^DgR;1lrBmL) z&QGcBqm0wqk&P=p|0xUjQVfCx`+ea(`)^0w|4*`j{}Fn{s-Lb{%LxBiQo0{J3^@s0 z>I5SYfyM^vB`_D@OQ~BBk%}dtV~`9NSE`DWo9;bU=W7hW$({g%Tb_msshhYy06q$W zgT|0qlXzy%*}n@tKBjAoCL8v_-;8gkw%Q#{d(t^yZ+w4V|ACwJ1Ab`^;-gcXFj)o8 z$IRlF64d518%>cxVMdUd*V*wCJ<6|PqVAd5)t4OcRQjO3~ zp6b7I6&%Q8^wMm^nx73GV)ppkS}=W5$9roU$&S&;LVe=s=UR+ryJ-x&GH*rPmcR9c zZ0e6qF$#SDjlhF?B-Ho89+oX|QyDu|;;OYb79RdX;Zu?4Xl3L;#F@#HK8~BqW-r3; z`VwG2E`}lLLvg)fe2Vx!b9 zTa*<$Iuoj+A;mDKoN6llSln=?geb+? zF~oa^Omkp02%*==9cn?nRZ9vZ5{z4B-R6ESI7z#W(s`UAb;{v471m^)T9+ji4Fd!8 zJYx^KHG?j@OB|m&JVko8I?1u$1Q5H0-*ooF_=75kA;A$zy#Q^7qa>GLHa#<NfF=F`Ftw7d@*gegOih*x(&0c#39a z&*etqNG(4gz+iqrR6A6?m)OdHPX0{s#S;G zVafWuL_R#~V11T0rPQ^R9ExFzlGd)uFe8zPlNaF9&H6*p*&A03onYo{SWKsVQhKr$ z?E6Uruf@?xq;c!Pk@A<+=^K#=dM6Xikb^I)ieyQvO~-Y4i!7$QEj%W zLvH@l+JTLsP zUx_G~vmL{bjHfA&J8=Qq_mGup4{&t`4v+T%nlZF=8EwbNTlhFX1I{f`(bvi1UrRWW z`~~#p3x5?xrvv&YY;J^q>#b7Rt=XjLkLB6Z@o;7d5Bva%WmbOY{Oxm8>d&X=|=6iplB8(Y>l~^&w%Jp zK}>7%viC4GxXiJpLd2xO_&PJPT^@A}H)bE)H#F#~yWuzZOQ=5OHFZ(;E2O-tl^jL z`NNFr4R@_9&O>RC2ZAETx<-= zs#LECQlKO}tHcKm0;4*Q1=~5WKjM0}%Ca?hOWvd8k3Z573%8~@7mI9j7-%0SUy6Q< zFp^=CM2d0f9rXZM(?AyhC324*b10tG$A~`Y*p{mB7k6Z8ooN)=Md1_#fjt1$VtY~> z<@|;@>IEMv+GA!f^o$czdWXKPlyqTnV{xUhCeOE`^qSadWnkxg7%w=*7t@@yZ z;)N*qZED^HvMrWxhsx_q*bLg)7UeUjo7m_z5v93xwSYJ3`b&mRQSodza4ou);Y0o4 z9^Np=5SiTz!#yi_e6mzbJbZo@JRVhG)-oQ-Iif>?&)7bL6T%tn5Rd?N9*cBBU|Y6= zIgiAwojQ+%Tv0sL4)!#Bf{c+)_-MoY9#AUOtsa)JA9jPnCOf5Rw*^;HlkO0#=RE&6 z^*&51i}{bNoaPsMTg0Y%aQI?iRkJ@u0)5Ol`94@B8yzz`7Sx*>1$xEX)rZWt|E%Pb zLwcF^$h2Ep8TY_$nJAXU-X10ccwm}aN!*ZX*d2?4Ze=dgikfBKa$^K%sq3D{tMG{P z9mNX;l^3}Z@mdojT?hE~+HApw;JMY_oZ$a_5sX%NkU|0n02qS#?=FJ>ugwT@`c7v5 zMNX(_|Ju~heOMr`OtdA#B$VrXnv})-mQbrD_~;EH!Q}4$@OBb~*3}s;PJ_+6lrVn3 zA?Y}ksO~%!%rxnj%s6?hrtiGsJ(Vo}$bDvQ6N5ppb_{04zINR@=DZ$u?a%)Be8b2? z>nV>SXerpMi-M#cutjx+h~p@xsn`n-D&tVpjO&XGJL7QL36?HMleZIF+f3@Kz`7pU zsSb1FblT|-0<-fj-KB#m4rYY}G<8vj7IZ0_rLD>(Xb21kKuPkS4hEcuCmizNwhlLt zc`AcyzY9AKPc3mx&Ky-gB#@m{SFl#)aa$%++bZ!lwceHlsLhZ=f#NJ7C^jFn@9VIh zT1h0hm+Eb>O0On-%Ar4^QU$=B;A-7uC+z#_Vv{>XuKtb18A%j6KSmTDn0KrS4y@K$8QLvfs*tLL_=iX~pdL%b7 z*U!LI?!@?K$6j)4_Us(hX0SItSLU zQhKW=RBitxhO53iJeFADVs-rXT!@CJ1o?8zw?1c82RTqQXC_CGp`kprUYKtxb9lb% zw>bE*HJG$7w3!$?a*b@e|C=DoLMmq_D@O~Ps=@#GV=zk|nU{K~xdfol#2W@%G(Vb+ zuqK!BQm-D1zoWZkdC1iWNz1+d1HO@Ho%kItWl$IMqWK-}`On}1bZPxxQA^0%5~CB$ zQhsjuyu;{K&l+YFhl|3~%3|tISAi2EC%X^40!NHYj~_SsfR2}xt8DxVMphr4s(O?G zNNRgeNF&d4zMqbmb`2Pv*h4nDU`QndH5~jW+cxeoH=Y9OA^WZxp;49QjK_uP(8bx9 z%g>r#$tNii^n>wFOAQ}mm-OSj?1B#dq9XoIGCVE9bV!)&JgTLLbOIpV^tN)yKTJi3 zLP&3J7=#JgMaOWSEipSgtXtSRq*+$`?pa@CTqa^=OQPg&;vw|HlBH#N+J*jdfY-zY`h{7KO}$kE@j*bi2jum5y%VVqaF&Hb{w_`g~E z|L6KW!T+Z`-G3x~{AYyZ|Ij^HEI%#-#1CJM3Njol%*S0p&mZk1wHG7_JwIf$Bn0tl zLKaFg2z_pj$~&1D4*uJZ!Jq;Mf+(F8m|9V@s(jmh@$bvy#}7Sgz$QWgA99$;ic~+8 z|3J$_L6;#cIYtBez_~XNdLiZMPJhj|&?2HmQf%hIdcP$op(MxOemfa3kaAKL77bY6 zHNLY)@!bBkeL@s*w5*MqiKEA(f4uJ*1yDNJO_t%tSrnS70y8j*B-<}B#HSjEk5@9Oa*MceIy$#Z$ZQ;|1-$j*C?;cA5#Oynp=sj zHPPZt(D<;L$rH_|Mvmp^JVb1Khds^tvd@Q zW09mtuv>~rm_2|1OWGclKR&XgOi0cu`1{@VKWO~VrNmsPUouwczb4h+v;JNG`!WQQ z|Eoi`wJ|X_b#^fR6@CJP{2zA(Xx5i27RxG{mfI24Ehwn~ z3melLDw>*=+i04d8^bj#Ho0Cuj=P!TnMR0mpNFZRHy$_aKaIcrly_5DvRvZoO64>V zbKPA;p*Q=*3R$>(_9A|>M;QRLhZ_K^_YQ%xP;uj?lWN__Fbw(f7L-LRM9n7v+B1~J zEfjpK4v|YLlJ+sk)dG3a7S{?@@dnk%rxSe%0Iqi&ft=DhwAV0z8RJ+mJ4B2u;AXJ4 z@dsT1xwB|u_b~vr$A65Oh=y6nJBrRy23XuYVPEfH69-9dq3>vJVC?WjrD9GTcyDKp z(MKKG`+-Tmg;lQT-EIBA-E+en-iG0lR70I3$m&Ld2?O zcqKbs!`&@=MT^(|fw-MGc761O?qTU$y-x^m}MQm z%9J))fBpvBMa-k_13KHLkhGmVhPwlY>Cx_S1$h(v(kH)}I*z^59{3hDUhOPyBkg;A z@NW0w^_7YM2IJMc8wGBX^xGWIjXU5c$7Og+3>Ym1nU%%*sv8HF_8B@71qQgHPsC$& zA3G8Rn$S;-4I`61dXWDoMdO>2%k<_r@OWzc_GjTyII-90L2=<6IP~2_AM=$Bzj>*HT^gsW- zC&BZt_$42zVOi*Z<^8nu$E-EKKvQpTZZB>&xAgSZL{hACHZ_wi>tIDbFV{&83nt{A zOnbmAjCUhI7Po?UhFz25F|Dz+_CBQ`anx3{R5UbQgi^|6sjjxw&997p&X%TJ{q?a^ zSlOJbt+q3-@82BRT5rrPO(2_TEq0U)TE96D9wC-g!Qvn4uFo$m#dKq8tQyL*jI6fi zXRr1EY1&+XTxLt-tl3!VG9k(c8A>Tv&#TEgm+>w&|2Ahtzht#EcDQ~fTyX1q*5Xv! zM87Z%6?9&yWisL&7~4t*wLA;d1dnR2lRp{>Tcby&GFnr4J7#9Dfkm?L?TwhRs%5NU z#;jcfdv9K;l@1!!F>jz;Y{N$c7hk)4AaV}amu%Rdy9yI-4srw2cXrHI2P5vA6;UHY zKCb}<#ShHg5*$Q^zU(C`Q&7Es_9@UVM%=qX5?|$*S>TK$l!7bX(PN z^q^f^q|0re&uFxeB@(`U&<`F&4?2T(RS#cNleP^-XX2KmZ;v)-B65*Z7bto4rZ7Z;|UY zqtb^9u<@TvvgFq1q!FCMP(0jhG?DN6+khmWZz(FoY2DL_h262dWB6*&h%LrBmr(b4 zaIlEk?l08Vx^c_^RC5n$2();`^r*4jHh8nK43A^wNKS>tI7Iwv88h-B09mULA!4_L z4*IBPEN~kJ>s#9t6l(7Ze`m-NwXzHRu7BBM{b8i1Cf!4*V`f{Rp*XxXKDc@57yQq^ zCyWoF)pH-(#kGQT7dWQ!^NNOhOvzqge-Xd=$}OJQ`@?K(3YXJ;CtI7NB|I7yDV z{jqp)y8aqM@_bbr%xHFolf-Sv^+q5|ly6OpFGZ9y`kCSK{anKW0uS3xMnF+w6U=8F z6k&zb13W)3$lQvLiOB(JQ);iIR!Di5QxRTNtA&JZ2EFIolbenQU(J*r|DiQK{f4b5 zOLH>`A*)RPJTliK>G&7Zv;ngVDh{g&~u5-W`6Id^(Nm5k_T5!DTfu& zU{-2&1+yaaEvPyTOh#hIE4meD5n=sC@*SF>r)W?G$uuvS*O@g7Q8O={r*H^no0nsv=V+9as8yus(ko>br&KmVJGT^H zC?NwEmPsWkDJXk2jo(6lJFtCt{?#XHRccZkjxBg!DN zcK}U08%VHrQ8oJu8%Z7B`6&4kRM;IuE!sI}?5c_nI+CT9Oazc(Az#Taan%J?&L@_e zCH4mZHwGd}a}m1$DitLX%^qXrXbH%)diw35%ddDwzB%HQ+uVAAQKw%tL>l8q3drPrGCP&jiw4-3#uBfB^ zk~5QXHcitZB}Y>(Go8Wa);M>0nh>mWHHtNPK~B zWbq7=Be!mV$h4+!AYUJryDaYH?x>qSKTQx9(;{(nIVAh}&q0mqPfIwTj9vQ6-+`qs zmvu>=DyA-PiaP+hHH2uAh1toD)`>z-!`I{%u^57dNnqTWDkB~aNbTyv$E$!N?g2+l znkT8{{Z8o|zyKNBk8g&wmaT5tjPNBSx%)J8g2dmi)n!idBj8zAa=ZyzN6skeHHL=0 z;^WH=j9GDCl$*F9iuT__AxCvjLb=X%yAf-inoo^Zsnpn19*nXA{8YH~VMRwmW_tHo zhcW_bbA)U-5{tQwsL{_wNA_RNp9dc}M3XVmJ;Oi}oJ6*Nb3iH`MX5Sg-RqzMG+0$J z-9QZM9z-8VTADro62;1z*MinOM>L}hH)F+gV4ZOO(exm=+aHnh zssaa(8$+mVB;-CIq1#7YGcN1wq)%3Z?HQKL`ULxQF_5My(oF5}c$a8B>+EXNxgc&6 z?B%od;6$4iKHOre*Vf>;4EpEy8-Si~ywYYnr?<97Xe(#g!kEob@*Vvum70pmonM6O z4x^hkPQVb{X0CH{(K?JoKNJ_@FC^g5AFvNfji5)+VrVgH8%{J*hoL6sVlElU0|s@N z4)lINzZ8#Xab&F~CXI#i%^f-Y3Iq=o>Hs>-fdTq+Dc8%&B^ozi|O=e*0|_}t#x zO3(3^VoJJ4wIK4mNWP?FUYA;P8f8({4<-0wZC@emtXRYPW5+o4UtCK#h~{lH*j}r`;G6yLHDYKRsKA zst&_5EmneL69=3$7`)kPb|UdavZpcyB2qEYD_6%Jkwv5*yj%3^jWxNS)xP^Rdc!q4z?Lo@OYh-=`Smvb}TJ@eJV zZ+lr!(2d-?Ce?!Pyi)DR7r`7CoQ>S4Ce^}ka#>I6jog5I9HfG5<1CpT$DNqSf9kNU*IC|H_jd`%^k6j7?%G7+dD^WwFdx9ZM6pi75PH9 zpi~RY2TX&<%QcBx#3JlVFlk$ZM@VE-o`DrDO)Y&KgsLzZXYigdx;abwrCJw?|4=u)u3WOgF#QAoN`Ali34IjgC7rp#?#g zp8&2-W>}a0UkOYj@{*Bj{uOcJ$5=tw&WrmSHa-;VSh?g#kvdEVC~mI+(n5fI#k={Ey0^bBv_YY@c@nA*!+aTM z6iJ`-4lvUBu7g#(z~!~Y!cI8Ki_c9#HhIQQgeu$6Fy4HGby*F;o~pd4QKz0y-g3D> zRX%*YAkP?dm3Z^9_@`WK^Rj29alW8tJs{w6Wj|RuF7vp#4-wz}jG<*GE>rNTWFdIeoNNMxuKf--{2*O; z8(-+sB3hZjYAO+K&Kw3y7-53XVZ4J6OhsC4{EkstxQ#yiy&%g#XZP+OC~n!H;Xcy6 z{?NrrQoW$gyce}Z$NSoO#GSks9BpKbo!wk&mBE$wD4*E(*zI4&aY9E>D=_m9o8x{M zpA9J{XrD22>TC-gshHqpm20X-XTr=T_2)EIToR_XDa+4l-eH&_W%VSbY@!}25Zxqj z-S`$M|B?s4)lrg%*#(;{!Zcp6MkAsMt~_JB-O&3mg*d_h4t=0)-q?^m(@+DBIl|mv zO{@T{GyzqC{I0FX5lM>Y2^4s1OX zr)G8Tqkb0P8_*_Ey3(xFvzA@CK3!ZJ*(t&XxF@5HEgbP!mabya50aSrMLC$NRSoNAQE_Dlf3r`Yv z4YaCk`Z(BOY^dRCoa+X}5B8d7BNN^9z?zuUm@x|+=k3!tQclDSvo|5c3Ky-9aEqTa zBOM}ugxf>S1f!L$c%;ehW-$w*3-!Ad^t2^Z|B?{vYs1(>*b{@(6T{sh1LdWM;XC5b zarwys0;CL}UG9Ze>g!?Y8*J0%u}vAllQiJfX$3mB0s#4ciCFKnmi+By27Py?IdXrZ z`3Mc+!mGp?GZip~Jwjpoi!+u=z;5@q7Wk}okr(jEVkp&DUl{g{tkAa*6@ZoToVV;a zpcL{+_k$P5F;w|cM1mPlNHXdy|6z}$7Q@ztS3(p1Xtt8y>kcokk|N?OCZ{2?u{oy! zha1CqIHZtN&ns=FcHF{pEC{E1!}JFo1qPG_llJ*;g2B?vf}nog^W(pV=C9S z8HQ+^0>d>)CfCB1d^Sm~OS;UNAQ_=hFt*(SXR-{=V2rHrlM_pDC_WN1$Si`jvDlEC za_1bL;V_aE;2;|Isu+00=lvoAKiYdI2KNxsz>G8luYw(|g{*M+AD=0f{s_mRwcYZH9^;6{XWkC8H6LY!JENM022N& zy`{+E(}E_JE?U*(ziXY&?*}BoqS@v2cpxjl%I<~d(P35U9(chR@k5Pi_QfSI89H^C z9v}twQky*+*+a^rvc{$KAOz*q(9#cftt1Y^?6o5Ju_)rSx+!&YB^RjMk%-Ti5SsN2 z1(&@aoqi##7EJ8r3^uhgvxPNG8<%d46Pkj*5>ukFbHQMJf~ZebCrw+gnMH^COQiZ+ zafFjv)=^~)DmL{~zVpJ+0LKDpjQ6L&Y-(__+?3zj!vt(YLyFYoi%%$#L+(~Y2tLVR z%z?c&>IY{2StQv32;39`?Mf%PM{VnOXNsK_-CGx6?pjWKp_V*%#E=bSufIv`j0yl$ z=~R~hq+T#N71Yx#o1TfiSKZ%_0hnzAYElKEU1^OWbb=%_>_{SZCJWTmBDvDlIiy5O z**3dcRsTXEmM zbZ9O;vJi#-XiTZxPOgMUpAs5L3g$8h;c4+2(G6j@7keXCb)=*WxOE}+ zidE4tS1?~a_`pwvF1z4RHC`oEb`exC-YtjNNfU39{JT-i^QkAJNjX6 z*UF-{W;dP)Y%N-JjTy=Go1D(LF*4POs3|X}FUw8}->6P6+lI1e#R=6gC$VTXE(q~N z+O@~Fh_p*!U-CKJIia;TRE@4(0PJA_^)kl1d%#5a@>|r+y{cuC!h3qEm3l$@gm<*X zE8t8Zq~uECA^OIXLF4aLzt3h%ya~wnNGMQW9kRbpR`xv6xcl>;hUrkbcGVEOl+_a?>Ctk6KRO9I)-f$wY$zT28Un8 z^_%y`hJAn>Y6j;%y?H=xos4H=3u(=&Y};Vh)V4|f%0*5cJ=Ml_?c}QA#*IBKNun3R z!zk!X_`c-&#(REp_4v3I`7x`@O36w9;jtpVGXoDyL z#I(dBlOW!p--zU5iqE&Kk^fTg9H0>{4A`9CvYbbpAFXtu-L;}qr(vmS$t}8smvhLT zQ&{@e#h5rwM3jT;d-XZ(wfo9*`^kNKoY{Rh!#GQTgW|^N2g8;X_}s?IcQ23dTHdBR z0uD4b6iC5Sa&QQEw3iRia1XiuDenG}6eb(VR&c-w=!Kg5Cl3lP@?!Ae$=#iUqCj^T z&B#k=h#o~3`G_|B{IKCZC@gXP%>h8SU$ybREW#fEb-yl5aotU7r@v|1gvZLLsSEBa`g3`X3gLWF>j?DywAG>^ z=aZ1q&h|KTC1r+E9fbi2$Dl{;&Wdr6p~>mQja^kCMHj2Z6(iz43*!#LwG?rb-FD0M zfqXXZDrYoEv?++?aM^`>cKp)o#2f<3+j-2QJR_CGR<2D3lj?yqYm*C>wTj@xg|s@+ z)SPFVc>W0$UZUp5MwAYv#mKPU0?T0I%7Rs?{_m*yt&Ful-IwfGd8j^nO7(V@g!{doE#Fgx z+69MRnYY3+nZ5wPnZD$vz9Co_>%}j`hGUt(gj$#kQ$zl^<%VnuZ-p~dM!fPO-8;_zfclIxNfC?t5v4`wn!DllnnXzQmKn92v+P=#BGqS6&EEm( z7}2eJGj#=}GHVS@LSO!f&CD;4GfMYc@jCXP>|e2B>h#+(f5FyT(A6so*4nYSP7p2J zd3KFFrB$I-z6FM?-iJi6GJhe|LXsA}QM2>$c)Ca0_5HIU4GZCgoSXP5y^VRy{oq=>f0TKK;VtD?W9=EmeW)hr>lDSzEA!Hmn8RrQ4|~!hdY5O zFk-GrE}%X{%bS-l67YozV~AP9k6%;FkjH*eCJHR-zjGmQTja8lJU5)kS8JEAir5h` zS7~)dAi5o#QmT2KNWOOR?i=;-5Uk1^FlBOJ3L_XD^|#XB07=3evTzEOoS#rhh$)lCgv*E zyigHhfz>E1LGbyK%$AAbq-USAbNe+%+_jtHTac(g3b$`%?5tI+><(^?&mdXuFxP2R z;v7h*)eN>07XbD))S8d@a_EUc_NmCCTCBbXImq(L+Pd;dTFUaec1jy;K0jJPGIOq^lBBSa@JY1E z5jW_0v^z1KVf9T!!A;_bo-f7 z`NlO9w(Q0d)zs#tL`GDxe?^_*PRsBovP8!SBv15sWp-&xE;xcBGLFJpJ$yi}r4r(1TLW`Ncwu9}bk)Upi{tTl zA}znO(|D*jBVrJ!UgJ9pXokXSxne6A=iZto%)?ff;q>%}7u{IW zwrwXBn-$v?+jga5+qP}nwr$%L^{E$q@BK#icl(a<_W5}d6caauN_;bI?z5Q*ju7D4&Qez6Hq0|vQtRLpT`|vhqDM#e>5&GED}ZDIN~0| z^0W`nH&!8A*UG++iM*qg^^$DlcWAY5Ijj%P zE~h=h)e4Q5mG3y#^JPb{&JKy@vlfQX^jayt;Cob)u0+dvLr+C~kQSArRk~PsT;xLV zTh#(ZQn1wfro$1hpVuH-CDdX&^?+@p)+bC}lsx1e`RNlp(!bM#U;i6D_^0i*p#i{$TTxdJAP0YY^!~XuHC_c;N5c^1 z-75B>;8<(XP-}3((%_3xY=d9-z)$|ch4QIrr>Z#JWDcv?Rc0coZ@8x}6h-7WNg?=F zTkz-+i7OQ5H2&T{e+F&601-a%f$@9nYINFPo=YW&1f1HvKR#yOrr&0MbjrNFy!+Z@ zqkP=xfjE^s6``Rlsf9~>NcIo57u8`d20!_QoN_mH`^vc4 z`_k=5P+r8|{JaZuqc7MQ{Z8HAgvP%{rzg-4@*O)ooe=kKD=Y4c>s3`865}HkC2sVM z99={ri^O$OP-TdVW*-t^2J$t_my8%k?Z+)sl+7zWnhXU043ujrgiS=@4x0MQqNj?G zOA6V>2eKG;qFfzQw)Eh@I|6PF9lx04ryI*`gO3ndHk?fZq1j~BKz8eRpOkVpM$>yk zlsW&#-Lzx4J8U>g$iAK? z`_-LGJ6qxN@9mDPHgfC2U?s~2mNPgqB|6#$5<5Q_hHFBCK>2=b^(cnH#%b<2} zU9MM?IS6O5U}kp2t?3=;uoO{zJsrslq&Q|yNo1&3+~N(x(R2oFKBL3~x55;xDNvK! zaWHqIK9E$Xda`M&gi0HYnm~WUAmZe6f4R4?nl&h8Oks9dMZn1iIx&ZmgxzDG4$D3a ziy3A#!OR#Zvp;ZR74bw6AX7PNu49u9KVZTTJW6LUG_EebwISexab~qe@ce%Mq|_Z! zFwt8hH{p&VZnqUNP0LPa2rRbGKXzgZw^&uW6=q7&8)WFd#4;3YBpH2g$#JTLIMnbR1yb9O$u$^6_P^fK= zk|s|iJZ0M|F3}g~Je=4MG6M>h-lWyvdn>O)qTWjaHn&&`eC1OX4qjgT_ZF`;MB+Qe z&8+D^K$%~qQRh%m{bq}s0yd$y@UBE*y?V?s->G-#2se zrTQ%UK3hy|8ee%?Ez!%r>%Y2D9`N#a=k)NT59c8)#+ox;zv9cR{lbyc7kkBbde!O; z?7O4#uPOE4^V-V9tU**7eZMT_??zm95+&!9q!eJs_*{NGdcf$$tio0PFjdymq$?eJ z#(i%wVyk}RWCap14^Q;{VpQ!dILvXHA=H>tL5(PuDM*`QI2EIQ1EWyk{sG8}tat3Y z@A0{UH3FH$9G*|>gcv4i8JX@OOEWI;{%yLo8J&#c$P)kVY3j&D0o>8DmAy|W#>9v{ zzz=v;pJ(2pV-jlgSC6H36L58ubwBK<%{eRag0f~3 z`;>i=Rs%#92_v?I+AOhxg0-;)+6qNgE650bNa?H`XP62Qn_Bf@vw5JRZVaYMu1_*m z6~FiuA6q;)-+kdDjmOrtj_H|xYIPfEk@+&pH9+8vcTIKIt)^_|(h6^7F}_|c=X zg+0<-l%px7ltPaK)R1b*2R+eW>_R5WEP--Y15sw(-D<(I8YJ6YL%Afm4;A#i_K_LW zwk3E@4+gjCoS+%r!*XeaW#+H1#gWAPBDJjRV11)K7#O&zY(pn21Tyf#b~!W zfAlM=X*P#?(NQk6!6LaH>l8x+suHln)dpgkSTv?m1ZF_3TXXvg7Pjykn-K-MFk9j0 z?o<4`ME<5PW@>&kh6p?8RS0g+(T7ItG)rtD0T)|s3vy$$@OsTgRNWIy-aZQkfpMl{ zNqJ(HSmr2c#XBCvkfl2C6z-{TskYX;xZt`(MAcjG>Jwm6cCjgDl*;& zOlTX~LK5E@ziUuE0h5#0&B&SLo92{}{u^0~5;hO|pk?J=00H1REs;1GZFx zlbbWaLf~$KOMe5+n+9Pj+CE_+PQ)LY76XkM z8wm|PiK~O;_y~*hdbs@!1J8}w4f(d%QYbm+Vxu0;W2S5+EcC)a&hi_=A{PZjm)%@v zbn;|dLRwusR`IF=jxE7ix;%gx;r8wwb~KK0_ZE;RLZe7UH((;9@Mo47!nf$;8m(BD z2TO)k?~n|mPqC-MlE=GrRv8di*gtuIXfwK|?*xN}x6wBlp@6XZt2=g8@%nEN8dPi& zcMTKc_1XA2q>*i7f|LlQQEfwl8W6B18dFA-ou%)~t8Jw1WOOVqu^-V_@a#UD?XrM` z&EzozOtL|?W}@|jj3GlL>*A6kL`L}xqJKo*E7GfAoMhzHC5hiLlun9>g zEq`M$Pn?~D%Gu~vAZ9n@tsRF@v%kHBI=rh}Ca9S1J#|VZ=;*j3<`I3%Fm~ybMczn3 z+VDVIRV`cz&gm9kEpvGgaJ=+gRZ8j6rW7!@nW}JL#@gP{(+bS;PsQ6N%G!-{y=YG& z!>MrHWgGXuSQz&0aLG(om>W)G44F;ETZgz5zc>&*Y{FZSQH)<`U{?nnhIm@e-7y>r z9EbU_I5tS78m7YjfT^%|n)U|1a62o=e4nc6u#{rP#g4~d9q`=O9Kuhf_F{Gj#W{VT z_lrQY@0++wZIIvb1h-Q-`{~NZAEVwz)bGK002{;0k^hvaf9?^_EzxBFsM7!aAObwI zwlk*Fwb9i#F{HDwHfA=XvoK%p?4Hbx4-YS zt8;&3_-c6Qa%k|pulIOj{IguJ+%7wbR)!N#X zj*f$-rp?C2^{%eN&d!65j{UZ_-QM1lo}T0O_T9F&?SX-_o}Q!L-lM6h`^m|>+1baG z)c(w@k-YrL%&g)3f~k_y`GUf!YQRraER|O*l$6dEm(0}FZ&U$(rE0OIb+@@?Cpon* zuV5-GXSAqfuCf|nojq6IxOsDPQ(IdL3kx_}_kVs6Jc9xb765;u{{83Q_v91A$4_7% zUqAnVz@XqD=TBh~;ZY$zpMV_W6BEK6W1JnWt!#M>wN2w3onzv(>~#cm9fX|uf#O2a z^}gHbix|mC)YOH<*%iP8T$>G?yh%AwxnO4~{doFq4s{M$n*Rh7JP? zb(9w)m7=0hN9BR$fSd@76ICRpgr_&U>gw7W2viT`h(|{Nkq89gM5dSh1kBGP7*{4c z)MMxy2nxfvhn`2Pi-U&9W@1AB6Nunb53uGCf0n2ZI2&je@|J~hwlqHD8WVo*0C9RZ zD@qRW^*{ua`gc+m_Gxc0EG376Tx3(zPheo`O)m7pvITkZKUQ4#j>si51CZ%;XDGJj zNSk87*J+BGE3-R*fq^~|yvqn$+ql}98k;!aGt$%3z!=OPT81S=+nXI~ZEp zQ{#(U>C@u#TUg-B|9+!AzPzEmp`DYV0WB=lH%SqEQF(l2Lpyv1T6$QhPn^G74TT#t z`(FU(I-veK+wteQjDKBre1N<2|FCZVb=lbijQT7rOdSk=FFyN6huSJhWF2@Nww|sl zz^c34?DTrN+U^fUd%Re0b-O=FW-^Yz>R1mLQmpdc=8j#q>f9GmZ&47&8QFPkr15J*j|cfB%yHeLTrS*ME# zCbUeVyr_nrYGL3A0m1BmxCAg5+dL@_4?Rh*f9Z7k@|3D<*5s&vv>=J-xD*3L&0SH6 z#>gcKSXhH(TBx$$(3L%jyRzgKJu>`mCGx>`nS!CQ?xEhXu_3gQ-oe(v;nw!{mV?s% z?$*x1_c|ebDdYVTr4X2fKgJlQg<5>S`VDOW$p7Qi%fDV-mjAb_3*bp@drMsl3vE+< zYpegbq>4JSI!_ayfJ=Mh&+5T0Hak1LKHgVWvJ69WzH^_V?dp0~_m$A9Z_Ag?k8*;+ z!NCF6(&RX$0u^te;)J&$aBcU0) zl52m=u2Rqpr+obi(acW8ds&yVcKG~YE9Y07R0Hg#uAWjHycU@m%YzgARKG+pqeXy^ zzcuDDh|v*ZZUzem%BHyGRG;|qrNrt%Txm5~4O#E_!9TF!@g{rw~!_s7R% zT;g_PwY*332m#Ib(?s|QK3zZ2{eX-U=+bKG?d`3X+t-&_z0lS~`*7GifGPHK{p#(k zf0)cB3-EGv~CTL@a&+Fi$=P%d-8oHJL~KYGw@qIk7T1)=iKo=@@lMT?K=Y_ zLzs^@NC>Z*su21+)6q}1EUxaZ>rC%x!2$hVAK!889(4v(D`*ZjmzjyPrmLazEa{-7 z$iRWniC|{TGx(;Ou4oPQ1kr2)MxtY#z^pVabYnjL&Z4p{RGj3`i$8#bM@?aXm66gw z$F*2Cjtq!#bmjK9MyOoQ9&Y4grAGY+vY9BM1|5e65LxERQG$n z9YSv~HSBqAd%qD{-r=gJesf z?%$cCX>hj9%}t0HDk-h3WXFyBf~-coZEiW!sua09Rfi4o{Q-%(j3k7U)bj9vb$jZh z3Poqac!=?!!=79uyEa&bboafvH7s1?nf=FkY*p*jeVeW0-a#SK4C)xm_<6XTWdvs(fV0?b&rt7*=+G*d7k%&maPi4qJJZN+rrSH^i)Gj=C_@VfP!aHEkO;Ce< zpL4nKmOxeWE3ox%wFtK4hu-SD!pCpLK2iB1YeORW%1wZ%=ouv$L|+b$X6UY`UQ z%fP>l%EXR#NKOdx66T@yWe2$t$_-3*%d;v{fS|W2lpF4d5=xN}oa0M_YlaE4#c_o| zKoH@2o?jKmFeP7Y<;}7TPcot8bG|5=G9;VSv=)FiV#&nFW)!)ubm~7!?&E6CakCY% zE!|c5a|oXD&M^3t;YF0sh4}&2hQxqnBXo#b8Yqt)J}>VEHSZ#H?Mi z)o3KU7OEDiPK%34KYNwo8P#;dn?<8jwyBz7)BqOkDFUS?JgxDYSyf{*X|KebeYxN% zNc@2evXz`Bd2_;>FWaK>czZL9*t#)@~XTgRF5Ha=!;r2PKa39v1P z!OP9bMN{CgOIomn)E=70!O`BSSeqC+=WZU6xQ-8~bUaG>rh~dEHm(T_Hb!QHCtR#C zRaKM=y?LzKQxkv!TMA=2o1hQojIMV)hAGH(X$2JkEj)@4IzJL{r91|k%DgVzXs}{l zU_9)Z7Y4DKFd0SD@_m{9acri6-lEY%f3T{NG(3gm!QU9!m@M&;rmziDwINgg=*pkw zTX#(BBz-h3vh!HVbM3YSlKyv~!;J-+M6SgKyrhVsVZc}lu3;#ca(?uI{8)PO7H*Cn zn`cGlkrKU&5Lx@lJ7uogx>5QlWil9hUV0-|=aLS)To30bYYPlFnL0f&@iCTdFjRzv zAnqg{rpAYuQ_zBa_~J#(BDt%85le3~gG0v1VDAu3MwoQV5&D^*%5|?gYV+N8>7OHC zzhoNsO{v+M@386aeD&c8u5?Om5y>#x%2>)B^U1;Ztateh1i`hj=8&TYQXoUZx#4|U1R1n+qXt8SG8a|S(27Om`P#<>F{`DswJLwh0AQIQU%NOuL zSV_h;uF_;BjA79vWB;BgVn|UJ%pW`S)!o+w73So&7T;ui^q~zSELEAwFS*EwkYMm< zpy2oqEoUG7>1vbbs(P!TOI#1qyU}S&fR{-Tp3Vp0P~wivGB8S1)IgziC14Ag4p~sh z%rSS_b>kaB9DS!`A2Me)nF|ayn9-Q&U?UD9H=;vnIc}YxM}~*eHVe4P>WIh7nwyt3 zIRZ`{iRjGkMob#y!`WR>NnnDbqT4j$(>WcS;`)NL6ScatyNil@;DD^6Efyu%FHp| zB1zrzCxXCf6bQP%62AF!g|T7)^Dn4hza)s^x;1RhTCi+m?1`b{Ry|kHPHWD)Ma4=j zn2^Ojr$!ci3#g*3G$7i78dd`K5aWwH`4mej-JJVE4!o8VOLHe_sGZC5Kz$z=vn=Sq z_)4*OZ7g~uI3>nIa95~^9n15K6_qH32dWz%U)TYE9+Z##mWrPE7~FP9m>Jp^UUxD! zLR?ckM?l+`VzNwPs(=dDf4-pWjH~O6_&^4gYBl@WonV9vLl!bPGD z{QDns@K}BP!z}?J%O22Q`oA+7^MA)=*2V&M*5-zGQr1?c4%T-6J95ddTOsnJbA!bB zJEHWlQYx2+K-JaMEtF!?5u5sC12&I9TaSGrf}!O!Z~}6L`M~Y-Q{MIevL%SosWydq zxnwZu(Rt|cZ1m&(*E@(US64+ib46iv4=l)6ON*_9@YSYRQu3vyPvpH^}wH#dI!y^~dl4vazH1E3YA)$^VKSX05H!LNJ7@U9He9Me%_NBgoqs8Cd z!p4R+gh(CISh71JrG(Q|q_N;!>K0ttvD>9L2oE^5?L6hNz8B`qgH7}Q9^v8D;q7%2 z;jVrK*E^yJ6**CE$XskWBx6#}!6th-K-tuTn%LBQd zf(a8(=YhEZC$tpjou;jDtyC;-%^*A(D<4E)>?HEwnnY*3@U$2@o!KvZV{d>Hg;&U= z@D8QC!Np(J_QU5XvFHj>HwMc4QI%vZDNzqrg|KAIXGv9rUgSY(4{eT)5O2s!Fo%n1 z2Vh+>v?uV)!Aj8lSv_w zq$FXR$}>!HjdZiUmGXSnY{u$OcK$N-XI5>sg(4^;w2uejGYdWjg6cxZX+Wz zqk3>G*w{k%sV$IkH2c4yF50Yj!>&2V_RwIJvRbW@LmQ0YqsN~BvuGc8kUbh0^L;P+ zg(XOLX6d&36Hd&%Qs)QDD z?-f7C_@&iyhR%@FVBDvYgK;gIBWORE5eEtN=g$}B5V5a?5n!nbh$7W47q$kiXFV>$)-x`yD=33o@iKE#BoFH^F#Z9(+c&} z8q414>1M}LLNhN~Qgv>2WWmY^<-laUXlV)z`T12}5BS;dJ9%!$V;gr~1OmCV;TZ&x zpr)|5BD-`28w>K_)iTwd0dx3J+K?0q<$8=e@M*2=-pkDhwzK`cwvlOrk@t0cnSwpkb(QvzAx)f)(2wqru`pFKyg`9*KR>pcZf!R$jw8L(M zu?W7#_|+!?dc=AG^RmZQU7J@$WWZ3HXAsxj<@jkN@J&(o2I`&NRgMlPVjxnyF{9s zzN*n%tLuM~GUOEZvl`@*0uxrZKQ3=npR-IBO;#L+29PUmkY|czFDv4eEoxK`G4#c1 zoajJ<9Y(O~y9T1=bp_^$7*~SG)z0Od2KZ^VPe5QSxjT8zUUx4f+9|Gw1wE*JLyv5= zkDhd|*T!*Z(8R(>G^Sv(kakO#Sij&fvW1SIWv4I#Fw|!lBBT`;b>IPxE(CdN2Kuw9T}h>#v1F zosG+drZlpwX<&0h51ohl2*#G}D`hR!VF79NFr9+a_H4ZRZUP915j8QpXi<-v-fKT-w2H#(a$Qm#--TPiUhC(Ec)ae?t=7|D7nvUj;FEsr)iu<} z+9oSG97e*p8xD=x$n{2ZbZ)m5E6n5KJ?2&zc9(|`$379_MjdM8>1&bUfJxdHA3EXo zF5twHIv2xl&2$jU+6!dBiHKOu+Zb``OA+RUG;knRGTAF;GzUig$+vBY9lH{_XCqc- zIjd4mBv6FOOYL3AXbV1V#e{d$Ya3zmi>*+n%Qfkf0h@Onk=EIqNh9L6(l6MG2a=c5@wL4iiQ>bT+A`eZH}Us2~a_> zr%rTf9ITmUyPt%=AD0>OkYu=honQc8;6Qcpdc*G5+>h%|wu9nnX4rS@)%K-aoQz`k zW#M=7p%O{Le@u0X@B*qTAy>yzty$we8tpt9RUH_QyHGZB#*!#jnBd?Z{PLUZu&?u3 zVF7rP_=oZKmsMxHf|m6xJu>(AZ?#6cIOfkAs@{w<73Ru7GFjrX-hN_V^RjuaeAed7 z6=qz%tx3O9=P?lDJ^{Uw_tkTHGs91by|?i2JYs7fu0GOf>*`{0!daA|MGewGU=<-3 zwVrRmU91$XG#Ts15>y7rDX;6AfHUGey#@seZV%dB+3XDlM!uR7M+l!l*j-V_Uka~8 zi{}~qO3JpmB9Ifz&WTdZ5JKKCG;zJ1o@;?=Za9Mu(1Rz@vu3;qzJtB&M^=M17}}+k zA!_G=KB&_Hr*EQ*70NqLGEUACGp`t%UjVt0-OCO!xKYmwWW)*O=?y>&dy0E`Rejc@ zMBL|Pi8^$%+G9kle)tMMZA6Hgjri6OMz-tMS3U+Osfhf-A7>h6k56Vw{#|Hge*gAY z3g#ui`Vb9_jgUQ1x6lw#&!LYCZFbf)*X}e^tmilRs!Uv4<<}W}0NN|Nb9vO7i$|`|dDkfywYzx&iCTU8_ z^YNw)#DSb@P2TPvin0h?nJDwLogTUc!|-Yj)w&1Sng>{SL#GQecMKKV@gnUW$2EFP zYs>(aI7z1#HQ&BJpmrQ0ru`WJHOPM$YJZt_#w%P}&&VQkTbDb`NyDd053VVu`4d=^ z|Cpgp7D9@(N*TNArYneph>P#Yr=fmVBSk{ueS&xbc+;5=OBTqP&OGiuwAoLzH(vhs zNX*(sP$x~WCHn%BO4?wf0{eKwy!KVD8w6w(92c_+!)|yglByL3R#XJ!BBN67LvwbeNT$2p)4ALNh!Chq$BLFN|@K>{d}ac(s}E1^x!URA}#G^Maa>| z_tsF_nMm!%H(S|B#CEDC zffwNN2a`%dqjP9g%R{CFp8O=jaP}TPtS!oNYXkBfbt$zmcCuW$uZNnRJtoYF=EkZs z##3`#zCW@Km^4c!qh9*I`$(S&QkxSY*Xx&=^Ow}c+on;)R|jb1$r87(s>-}2C?vpY z%B0IO$P@qgqY+K1AB;~9fFl1t48^}JJLCV5c*tqtf@|oBetg+eaj2jB#Rb>+UO-C} z38SJk`6x#~=uCDk;oRK$=0yI?Y_%OoPPQ;7}*U)kg&GKG8NTWj1iw zJ@sQAD_S97o|iPQ42qJ`*Qj_c^;V35+SqaF*7H}w!(|!1>uEV5b4)Hzq;Mj7(Wdg- zBO}U2zuNBC_xVl5pVD*h5QABwgcA;xs)vTBS}jx@>jb~XkIRcti+JHqhNoMDBxn`a;{%emtmVmp;R4)l_yKuo~kPTrmT>lq$%~E zuvo1G4Nmd@0gHcGcE-!ES^*ptxhD$N4X{4PCD@so{^arFGbaS1%YrK66HqtF&*|V? zHQlMI4_y@R@BH-f6I5}>`^#1?{4NQwAG|#EGHzzN{Xy8`W{1~v$mi3h3>|Q^)1n=D zpHbC_YQ)`dVM^X=yi@%vmfqoWEgg!Ylvq(?mLC(c2N2Htsu5!n9SlGCz8ja_0LErgOj*j1+3U+>wHQ8%XC<2n(uo9e+tDe}85{7_CyzYF&2Do6 zm3CKs0>zvRBWR2-1sL45C@6hD-PpxVp}bv5!Lfr_z?tdjGULQ4sg5~WlE93;zL875 zA@0l9>LyM2NFu?NX^-nx3 z+Ol`UOw7ymKZBxGA?vdb^rKS&*Z#-~t;YgBEq3%QhErn2PhNc=&mMcU94X!RI)85zWUmACM6N=eCzo`h8+Z zz>F#WlCrSZ4OrP%7w9uHE|#DblZr5Zm_uPn+d}&|H?z_~JF)j;F8J4L)2Yu!;Niu)gr`^Nx-4Pn{(4K#y;`UC42{NkDZxul~NYK_^X)02Cmmvd1q5?x~V%7e{Br{h1_1 zN_ek8@8rkn3`Ieo)soUVjW5_A*OLEmIu}~%ar0r4+%sur|~7DJ0bVp zQLn^}hc$4-I?N!sP}_3vQxnJ`3@msyB#XTg^dYo21Vd?^``;A7u`>&xh>qZKwQtvQ z8g85gGxv51i0ZFV2T#BUuGG({E)~6AD7cWQulqK2y{A7NA_zFX0`S1Im$uV+NXRkp zn<88R6cO~p3@WX^xp)C2!xKOeK=CraDFQc=hbW@s^-qdm)X(rFu%XpssCuIC_e}c( z4oz`QwPgS}kp07OVE?N^3V;JL12XsWvJ=Pd%KXZ&<&_do)y9>ktS>ppW^(Wa%4HAS zobBu59O-kCPNBRKS0El&5JA6U`%;?#fZ&W89v|jD%t+n4eT>Y6-zKfdPwMdpb<~u! zg$!j#(9?{lrlvpt)uP@`Qk8+R8hawP?-}ENypB7xvlW4D%0WuSH0!Q2E>W{uFgLN} z4m^BuI&(jKoc0?JUqLy_hYcz&A?F&$cp#aXv}R(rbra9*=V{QBM=#!ZW<;SKH>}N;% z0zwAr?~rkkl0AML{SbimPTPLb44bEFia#J3Mg^A(XbIXYOmZhJ|KlFbUxlN5#dGOD zEQuB>v_znVxX{fW(1taJo(WIurU@+2QrmCl~bUI`AHn0}3-$AQt*jY_zsXc#)fjDnT zr<&-kub@?R!CX5FqY$`g$$pcr*wR+?`c+=ZV-nIsnEVg$u#|5dr8xoApmJF9nNeoqrlz8%SMv|eisfOJA z&CmfebnQuF98Dt5y1~)qz5gbfVgIKxlmK{aC7F}t;%G;iQR%QXhT4r1I3`!W6Q zE|3UeF6JvSvP8BJvDlk-TZ5y;a2gdC*i$u2!4;rkyC`*2*YBbU!QX$H`kFU9FNV5oZkk2TCPh_8&tpC zFVb7Q?wj4#pgl4tgg1CvE*Vy7Xz$7a5M52E>=-Sb^Wy-78#@JK7kCb_A21YGa0ZD2peh3HC}_v# zGuEC246oq7>r^%sf^{AMl}rx)?>dnGMARJ63(H4W(W8czZ0(?3WCdfD*LRn1msQI&uyShzXXI_65lxW>gupvo)!Vc) z{of%F$NSgDX%B$%03pyUpb@NLC3aYo6TavoNf6Pbpe2i}cb9hAbAB8nSwgDd2E?$Q zH?q6r56a8U!EcQ%tsxd^AT4QgvX6B)DH$_W;39H2IAg$pqHdh^#-u2gPRm0;k>2=1 z(aHno1eU7$svW~uV;sPm8@Ph?{i*dX;E**cYo0GmJuDu)B8LQ@%z|%AvMRbXhb+?x zB97fV--|=S@i@p@0^jU~v_eMyo6f+@C_%i{W4x2eHD}7sOXdOlA?H>*94{po(%6$% z?=)*B$jtMnX*I9Fk<=*r5S2;cJFr4_p05WiYOn+VNkku6t6%Qk_EM&PNJDvv@TUBY z3)fQ^+w(=R{6V5T0BnHk#bZme%R=W`0&4Rb2ilEP1PsXF6G9c|0C-qtv#{KAl)qLJ(*%?)Kku~Cjq1tGypi{D>7lKGR-{C72^lNbrz6x_#5Ju zW`D^@{j41>>O-_0z_zmO=zqttQxwUW-|BR9MgjnU-e0 zzRPaF;$|)99(mL#k9c)NwR5zJXBMt)23y8}x>QItfl;PN{X=G+=f@wa$i6ucw+ave zssC*R{GW5g-@TLnB$6@1%NHgWL{-9r35dVT@LcgQq+O$}bfNKVgtss-ik z`t8UkF5FfNj)Fh{U2A`M1`tK9&OBZpPml;!Iz-*T6JO;9{Xx;HG4%l%2>K~-=#RY7 z@l;@qV#j91N<~&ewWZeuRn*>9yOQIeG)c?%uI0qN4AtCrM`zF!_sVd*=tYduwRxCJ zC=2Jq*ya{G*~?WBM3cgmsoCM}pKH)8H8FPl-pc}vBaba5c~0-5srO*jkU2YZk2}(K@%1+w>ad_(EgluIOAw*U-_HZ4EG&|23RDrF zzz~F?&iQM*lvWKoyA=d4bnus8h##MLp=@y|`92e{#O$8#x=(N~J~UoxcjQE*Heu>R zmP&y(!S50-2$TgjA?pcdZ4b1pwUJcCqo<1Bi{$IaG!I`$8}PJ{8UN0j`V1n=U8S5B zkykL>wX>Q(Ds1%aI1J+MJ#O(vlMRvJ-q13g{+0c?8CX|IkY;X!iaYR}k28uLK ziG0RWQw`Np-^xb}=#D2-Jq7pt#?{DXej`_-KYBWCGbq0+Ea6P{<$+u; zWkrub^)f{t$|2miexH4hzSJKjs{5e;LWgEQd!zzN=%!MAB;N1PK`ILuQwu-^^FNG= zzZ((q=Vrg9a%|bM5tdYN!;gAavpCgy{d^^UiX}6C)q42jkKqR=Cfim8qe4iYge{1t zUWlvgKF(Dge#N1#vkmu$4g1ODWjx+aPf?UdTTTI3l@m7KQYO`~DnE0A1i?B%O|+tB zZ(5EopvwCC3dIqMjQGjP4xZ$-zOd{w%%oKMu~urg8E59P=Ei3V-fowI7vOsiJ=BxT zy{W+F>&GS+XON()ztidJiR1BW-OMx2`AKxsep0z*KstShM1SQ2h#fBRhX##IO2~af zD~hq4Ulx}I6-cC0Jrd8ns{`xks@B2-&GPavImL-a(mlQ>wB6ZKs8j{@f1p4LaobxU ztxSKvdd>CH?Xd=NcWG1Co85vc~7-@VX zAd%KNt_;!FV%t$bE^=ybcApJHY?6f;ds4iCE^i8km4d5Ap0Zayq`<>jdHu5Rv?H)V zW1YT<@LF<}A?$lwi*0Fl5pA+h)i<&}S>!=&K5;$!NaW&2ItWdsl+^niLy}@sLpvtZ z)bPXJQy^dI&1Wl)>PFjVun0oty}R*e=$Jxxe$63c=|hnY-u^GJ-%#<;+RhvTK*iu6 zhRWZKg1=;^bi=yApS8HzQ9vzDK`4_C1<)R?)67#e)lMHLwq2@--YvNLTV_E0DLQLn z7I$~qjVG4gAKpe);%u=}ArEZwLq4k@Y4&~#n@3;NFM}Lw&BnD}M_7^$#}T_1F%*nx z##qA|ZtJ8XOOYP_U3bv{)LraaT%^>>));kN-`Jkx3T8`?)mn~fQtZ^`3 z;|`vnepg;dT`GFKNN^!1o=&$-A{T*7Jp)YEUxQ{|qU;ksGD3@d!6N9ST^;|bybSsm zF9-rEFZuUC`7-1mzhx&5VMN5cZ0W0fsZKJ(WCt=^qAhLJ6T{zGcH0nB#c$d94;SJX z|E|_6ddbWJnwVcs2EN(a6K44+3ghgGf&n2h5QoI*a4L5(D| ziwB4(an~I+LBI@Zu@CL;_8ZNQ8M6A3#Fx%eYfi4CL znv9Xl)FWzRh76NY)8X{Vu47e45=xIQsSvQ+!ZjT;cR9XFm-NV5*<9KS+LX_7MIm8= zH1iyS6~8DJV6%IFs%8YnZRn^{-itD|4(eErr05L2=u&SuslN!E-@FAe3TuONt?W`V z+tQgP2VitDl5`G#X&!7{K?)>X z*a$_wSvi|^!eC)|=7smg890yF*p(woC+r7oXL466?`g7vS6Y_yUV<7I`#SNp(L|bU zNWa_LH<}t1#|1w&ps}avz|1x;vl&KTsat8vW;2wpBt-*3yyui~SZ}zSF@sIDyTq4; zn%>3JtXG!p)Xlh)0(hL>@p$S-k_o>EJxoFv&+S`W4hP=nxzGk=>uB8EVm;CCGbGh* zUriI3u|CLJXQk73t6d#q0?w?k#Gy!g?CBHRdg$WT5ic@J;I%^kW<670J|$QUVOYK zDGJ0UVRUuoZPFj~3(3RuxD2b^(_oOAmUqp1aVer$%azy zK?RLbLL7h-__{;rb?fOZ`%PIM;|^t5&>C%0OOT6h#U=B(H^tQoikb3a4*+e@KXNFp zPOU>TKvEt3&$dAs|L!y>YADR|p>sn=PoZb8`CW^k^w~w*pnwVG|J*^gq3)%swM=A1 zkC`wU-RIvFdSjm;=iqq)d6kRUEVu`USs5ce(5R>C(%*GmS`M)9|^&tp7A-vYl8u1yHAbiyZOd#P9G^&DL{Fwx87Z+8A&@?H-FSc3`SKUSV&oqO5Mz3_4cRQMEu6W;Jnqi~qu_8QcDNE#Q? z5>4i^(CN|VFU1g2GXnOdxmE~?*eb!za#3*+ADx)x#I8wNmu?(SYx6qZHDsrs!C{U^ zmm`>OAuV-niO#i0vY{QX@?oRXO^fe_qM)|eu*+AMq&Vo<)|WVb_NA54(c1z!;mPWl zH{o8q!hoe(!R7EN{tz`OlN^lD!P)iztMzhk7y6;wCa~rbPP+@gK7a*-Va;IG7D_0t z=3%;Ago( zt~E-qpBJQS9ga`q97mHKYgc@oK0;_Fc2oY5p~Sy;91vCQ76dVusKZEUw8Q{tNB4wf zXdE~)sVS@sh6YKwV#*&H9#Q&>kwybMZR=%_WFeGyYrm@-wwb+Z$O`sE6TulhDE2jw zUn%3HphtGkb)9 zRxMo=G44f608JkS3yb`aV^N8rB;CPURU%%=0$R#isQ-~r_Zu)au4A0lI6%F z5Ik6qCWv;zUNbf_xF?1WP>wjAMT+*eW_MQeP`PSg_%Sqyai5w}7c17&4}*25R&9^e zBu%~A9LdF+RBg3aIat}PZ^ua;m&@0Pd0oBBO=Aru%MgKcviV>sIUUOAWbBkXmnl92 z-7Z%FEAv@~U{7|EGW!)^h+gtbM64tb`9ODt1vB-JK0!Q>{Q0nTLB?&twxrx^%Qd08 z%?>)_&<&2@cj}X&Hs*1foUge_^otR!)M?T^QqODO0?du%$7+@ZoYnevUYw6wXf6O* zQ(?Xo8u(#@5k*r;Yr&G1F7yY|y{`TonN!-O08ilkd2lD96P#Ss4z&nX{UQd{6DT#? z14gJUgGq>w=P%$r7zJdwCb@fVygD`O-$78#%ZC>AEs!nV#E&xzfw}plt?9=zGdU~5 zS3P5-aTQOCeuwm}5_dE~5w7S=n?$Bw{F~3u!NI+@aG((e5%oyT$X9qn;^z_a7dH<` z|9Al>u&8jme1pyZzYO-jrKUn9P1)~e=@U$4v9e4HnQmByW*OBe*FOwjgOZpOG?Jt} z?!<*We2iAQNXsV)LS-)iZgVf1F>3{t86begnwskPyYnt{ZT$23i-Op9abv+T47(*P57`IJQ1I2)@O)z(4<&WS=tZ)xfbK?_zyw|ME|9|-c&lZQ`Y`-m9%9X+NLxnrjQaBQ z%%Eu}dpY&LtXM zJrWpk*I7ocG&r_Y6Q87_5|}VHlQ~WkONS6y(wA8XVtKDh6&5Ot8u$ViyL0gFAvvM@ zi#19p)8RRBAekWOUR~XF5gbPwjT|_hh5j-uHt(Hb1&k<3G={k0l&#MY z%dPE!0C&LV>w)TFqLA=+^!bzVWThIB{M6FaxJ~@JeYaF$^2Q%bC!+OBiTv_~%0Sn+ zU>o`a#*sY>p+)4~y+#qiPt$h?z2GZRCi}yk_Bjf$HN2X=)??_1i?2JRmB4tFH6CkXSQo2IP zzR{(Bv*#73G}yoA8D#7^f@%=$9!rj;70^Qw$Aj?J05Bv;!?OpV4z)u`V*Uw zj8lm3lmxninj0Y--AMQP`-^ZVUkmmiCP}bdOl0H^kyNCmDa+~FTG?;tht%VeI$zoK zY)7{mv@CF@xVF=FWd_LGO5qJ zJe$8=FQw-23fAvzIXr!KKJ>J3Pd+XgTOnL(VtU+U09wFf$BEe_=MtRDlpe6R3hy^e zP;Hh#I!xcZ7Vq}HidL?kmg_0qCaySNQS!hHTrQY`5xmVo!126wqMi;`vw9@>L|f@2 z@JYSPkI9L+J&BB8#L1WZ_DN55EMoCAMhS5Cv>i*giMuXuOx(AFJl)RV3>te}kx+?T zRfcfgz`$oetN*JlzSrhSqfPqQgf*0 zrFkT|&0Oi2@9fTd1n+o5*XKZo!xb40OA-v?*$e^;nu`j1ejWd2<*(0u^f%P!hE z4wxt+C2FHJ{5?qW#q;yX%q0XjNGnOX)Hbf;F6{So&z92ch%(UGA81*8eb1A~^0CDm z6E24xSx+gZub*q_t|0~>aMbk%_)(}@4Kckrq4l7^LZm@^Mz}0H#m?h`(0iz)G4i_w zp%h3<(psiwNtQY^9E0>4@cIMx*=+|bm(O{OEtk(_ij#n$Cbwf`VPjh4DYs^PAla~M zR-yK1!Wc^}4`ad9M{%A$`Ya1*6T8r!;V_iRX_afB&SrU`cN+58L zouCYhtR8pFvS>VWTgj*F6$Q6h7T>N9O=q?7>uvo#ZfvN5fi!fN(rjo5=8B;ag*hTG z!+NQ&(leDhyxJV?j>U1$#11q0*pv}1dDbrd@tDP4@2?u40UI}%^u0kZlO1dJ4u@w7 zZ{`Lv`C*0L2>=E)bhBVjiFO&^t$G`$iV=iWlJ3?{eMXJ-jF!rbwLqh$Vyb?xqX0iP zms|A7J@2LSkob-#3|2R2T=_d%x44)&&HKSC+$J$Uy3(6!BY^dF6$R_QH0zf)&M|RI zxC;|8Rb=8OxD?z4;jPA-)0{2SOZW31+*Q}54m+?T<}l|F2`8~Df<>w@db0`W3j&WH zhWc)BGT>%9(pUJkz;`qTvSNA81f6h}a7799wNP)Svo_4~v(X_K*)jCIMAlwCfj7!% zv6X#zD6u`DS_S(Iwf8%S1jKf!n?A2!;rJyCRomsX$;;)2*Es)pHq}>ps1## z+s@N;PVo?(C8=;faXhhV4Ix&#W)mcE*$JmYmGM)DqqZcY$JIQV?Q%g&u5KJa^Qe#Q zq&G-rd9ImklFG85p-OuW-BX)4WrRt8*A9iA394&nDO4Pe^w0W;d4xe69G+ukd(|Nx zIk$Z+8JzIj+NbiYFmTSKYq=pJ>N2&N*ul~$7Kyef@_-o=*{|8<(W@JOJ#?NXb@ehz zc9@t|lz8t4CN_)ST9~frt~?aa5wNt z;wqPZtRaVJ3m-x*6S$u%k=N-R0#%f!niF?y>O`SCUFJ1VeG6ka>e`=(m!cf?VRZQ0 z$cHSxmwZ5eF1A~ll83(}a)+}5Kj_30Sl*)!)NW3-MFdyFWEWzgL0ZnHdLLq5fg2cUyhiWN;C>_76^m2w!|INC0HG^ zGjqDr$Sre5P0*%RPg2_oy3j$5C%5KP*h1~AItPG&`O^a?0#qX7rb+hIMH^HcX{Pg9CqEL@iN<;KKY*Wb6(As8#E^dWzeh-bq$*eCXpx1$XRMM1iHY0AJ zV7;{lb2q)9*6I<6&;dt0E;%9c^Taq72L$V{C*Y|#mjP7o5vsTluENyLPh&V1yMfS8 ztWN?8eqRZB-sg@5r_=%QzA~Kcfq9`h@)9?v2&q!Or8I>bE`_zKUDt$^{yAsN1{$EI ziZz&tZr~}!LTzJ|7f8X}-L)^3WGzx}zwHi;9d~f+Y?~&Y8~M|nsld@ZZYyO*fP52( zc^0Gr#I(^rAtF8kQlMaBt|(}!--=gaXpuoCtar=*LvJ70uncNBbyT$mqT6ey2%_=KG~FbOz-W_@#?n^8_kmS>MPKWS^-P&EUQ} zb}FyH6vZtT6PexbnKt#8p!IWzb>H~_)MM)0@8&J<4zX{tOd2pt&>VxYbHqFMRMQO{ zaeGO)X6QEjm54pa)CmD?{8L?N@$;Y7kpc;3AL(yg-Tud!>3`iA{98LLRQ^{xY@(^A zt_q4$g&q@IsoOEp*{pt+2UTE)v}1c9fmGDgD?&%sA8zK{yyo zM=gm(P+H@wP`DB(xG3F>K=tfE8yjqZKx{<)oN9($Lc=(6O(%%pz2Yy8;xW2iz#%6lG@=z zj@sMLLH$8a()7|IAo_>Fsd{(zDn})bu4W#nnn)k@=KE>S7h@S?2hsZmVBVPRVGJQ1 zw1%9K*x=_F^bHgsYzYks!!trDq|cg-4Zz%y>Y6JIC;M^M7wo6{xi1y7E2<=+FS-=i z8>;(pM&K>8d?ds6{Vm{CFBTTGQb#qm8Os+h-F9ssRwdsaq~$92d{4>QI?h7F_1}aX zIL5|me>Z-(DmhRGNb?_>w+V|ZYWv0}7vZwy(DzjT4KaD9d_r%fND#FTrfg`gUDr*G zd8gPC1%C)Su$$xPsS_BRT9B(`!zu3Ly=Hei2+m!puH^Ani7t16Qk*Z z5_9k7U#d6lmn?ADsMtl)HkU`#paiW%*-HiU-@1kgU~iLLoUc=n#2lw$WL{59;3^qO9*g_F8oX4|VQAqtd zeIxK(Q9}9=^L)85|N1}3hABcy{aeWG1OK09wEy+d{g0L#JNA2xA35mDUXXUsnm`Nk zUJ#915E31}T*FuZUV&szQf9zs7dr!@k_;0I-d3a#2oVZ52Tu7R!mtF9E1Eg^?kxK% zgX8Y#Y7dEWd9XDcgPKi#z!Hs0t-`P(iVdj2s$e!!+b@$peK;8jBWgI>*@^yf?@s7T za)t$iZ+a$n2F>WVa~`Ah_ixc}Y!a+_(to92%GE9*&l|?{S*Jisd3cEZRQ)86&km-3 zrs6rk*UdsYk?%2?CqzQkSU`P8NIa294Bs210{Ta7O2)B*-vdPfZ>jPU#MR52(1W*< zuQk$yzNNK&7iUsPv!S&m$yq`!=P+XpcTEnh8NFoVb@K#7gk zlPVZ--5SCxu8<8FfFa7v{@b9Sq67dVq1X0Qw%>);n97(ZunRkPx*bQ>fnu^@S#u_M z$m9?gdzVA>kYyx~-Jn~~(Lo6m9YeO_y1)4e6sRR%w4dk^TZX5SO?E7;Lmb-!GBJ4g zOKV_D#wcIb-OWF(rg`q!b7JvXi7 z(!J}Su5G;#Mn@7;5UaA8HFUgC2kkcj;^aFE#z#E1pn19BI`Bew>=CVmx7tBU^AEuT zm_iTPO2Rc|`((M8jQ2z(AtVp8WY1web!b!{(Wz3w&?7wl&@ZicVNLlyQpN0kxpK+0 zaIh`64SZ0f4?56#^iO?q#T|d(g8c~_{sz(T=F2d}T8WOraY0kLlx*%9UV?C+v(J$m z@25EwXbJQwo-Me?l~&TrcPE7!Yq8j4sG=_(y1R&7G|EA{1mL}V$ZtXJ(a@|ka{IFX zdak*q-t{5VDrw(w91{jzGgp#fFE%$F>Ibu#r9sn-)qn9y3Q&=4MiA;<%qx1 z?t9zm>Vippm?|w^ppt(-FkZJ9fyss)LMdcT{o~|$DpM1V@7w1A72~%y0y}BI0f$B9 zuJtXBmo*=5bK0;|Ct0o6*O=@jedjpz3Y3t_!5}U?9Qav13`fUZZgk=~vwp~n2N%5K zpYSS-SSoP+BgbI_ZJ2Gdzg3LWieW4uW7$M(xJ7a?G&rO;jtZH=7mAvC3sA2P z#NO`3-k9>`AHfv+H7i9kkWu#QBicUeOtLjJ^bY#X0_<`^p~z!yqz$m<+;)??W`|(Z z-&8l|2JO`)zXFidPU8+|%@yW9OEz|}5FxBQ4iqyp%?7j16tN(d!#Kmhc>G>=1T-xy z%u0d-_*p4-&1>De-N4*vQfSl8e%-0Ho6wIr__=CQV?`>yK+CItft(qJrS7>#F;s6F zWIY{4%a1Edt7kwIZO20)RieGK6dN4;T0~$pG#X+z3}kTVGB*qaQ`sWVJBb~P-Yf9SN5zw(B2mScW& zN(Zf2RWv|-8Fkod&>C_0O+YyMtxZN@%81EGswY~LaXpEW{&hNq0RoD@+Z$P=fK@1! zxIbxCeEfh*OHilL30_QA!r7of%Gz%wtc&QWkPNO~>`U+*^aGu?flzcqz_VZgkZ6;+#Be`NrY8;b?adA;6#Y@qZ;O(@ZnNy8W@-rOfQ zqIEO=5VoP7|71Dg1AnGp8yUSU3w@VJwNqb&=C#aF$}4H6{iA|(`A91hmt_(N;}h?NMAh=iFrbwNrPdeBKjlaL345am+b-cfA*@%3Q6lX-+%Q4aqPtj<5s;o0;y-h=fIpdx2y zcuIx3N+ep{B7kZ;ibxs*P8oi?(397HD&mX?65Dj&Kzjb~0{I_{+JAsV{3npa8V&yd zayHqr0YUg*Kox>y7+_$hLnI=}mXe+}`o$QNrD4y`ySZ zwSaYxe7dHUDrU?&J0nl;d6G@<85l)dEq2(tV!DC+ki+-T9rsKVCSp} zvqT8#f|tLHryjCt-WKM5w>1=R0(qfrOrS(&IlwHomU&ALyL+g-(1}(w`W$ z*d~76Af!gK|{%Nq?%QO#wU?7(9N_}&m>^Qpo zeEoJ8ajy?$GJ2IK*+H@|54KIW69h3(t33Xfy~t^ z2DBh-d~f6A#c!gDTackH?W79uoF&JaiW6z8=w5X%g`+$>8M({iwpR_&F~vyzO&Kh- z`)E|zkcJj_H{xGsNhhsU5`UUI`U>=*;y^sam=we}h#y z6Vv)4B5*={LW(YV1_8ycjHILeD@a8Wvo<TrDD0=bQh@KCEckR?3eJ zBc1!DW|uP~4TnXYKkzGUkO5T*!&u4c9)qA%lO})Sm!@c&JvBHZ@rWyJfCrp#K7^a$o z9D^;)2i#-qTg`uRbi@Zl05^_l*HBPV%(Q^rWRN?=vF7>ah_Epxdz>}1VVCf~kyORa zaY0(f7^+xH(|Zl~kDR*$4*>fBP=Y;CN}If0lhaLs0UG90m9gfD<_+dc`baMCL2Xng zbXP_D=&9ypN*N(r-|2K5;N_DnX+$lH;2<^LM>Z-^$Fr^m@DpHSe$4=ANhhwFkv` z_Y4&hWxWQsya$T_H>sf7>>wqd>x`fJUiz528rLE6CYZV}5mgZ98%pnUjGx9{LQ*e_ z%$;@H=6hShcfQ5Xo6J7d+Z_4hWPdF7*B$-kdrIh~38^ zEi)F8#DQA5D1*js&~z_5T~ zwNoS(fSO574fDC4;=G0xH_MrqK?{jWh)@jHuAE|dy&8wan?ubg*(t=Hg<^TlGBRfS zB66_06(*1{fWHqbE61iCRsO- z6au4`Gm!rYDl=mcmLrS3hlE9=LM6ytyg;^E4ErYBMiJGZ*75zZ33rj?PszGSIE(SS zNLb(cQL~pV!uF_LkaPJq$$rxhgraK7%JQ**wpRRLk!1vZtw7zSZ>=;GXOW#iE()EK z4;a7zu*YjbV?KQBQcGpfOx}b#)`*f3Ox}gw(Jby9Z-MgK6HGJ@BOgykk&(RoZBBu7 zGP~GC3?!sw0j<0?VyvS-Sw7&cnILO?>qp_WFO=0fKvI3{XDJX^O55cO&y>B_jusB> zF~I!+|BhAM92=Hi+>G%Ny}X0el(MT5uKnA>Ienm!N(!|p{&quPAQR9$X0Q~auT69b z*u|y1ER=tWjkqdW{GGjgfMky-RwR4=7|H{1>o#!RhP<&xi}ajEhI;|==zzWXwQ`hi zq!B%HHdNx)izpjbBwe#)&}P19b5k$-9(HPK_E-mSn>2t6KPqwa^Uz@)km9{zWWn;@ zUE;mL%61zd^EpH6x(ikH-et*t<{5;|<=)^-3!?JcAp0~6sx z^;z~|p^Elm0mru5Y0{%cmBTc${AHsvyKUM725tjX(7>0#zU0sFCkT9rgrA|!&(rGE z+sI7pHb6UAMBmun%0j=au}W*9goCWHD(&Q)+G#Q$+J;x$1aksG*Dn6;#bSOYLGYz= zIVf@0Bo$@bO9&lPqMIC-#c5$(Mz+-Nwm^rL^loB?U!O)ajH#hL)Ws9b*|X@2g+cnb z_#8~?7RKhjND0k7JV9au1x^Q!4Q-zGz$}_(el_%i*%@9O&|DST5yw}+gTViD>J215 z4spzZR5tfR;P_)v1o%_jP|8~mN`Xyp<5py0UJDU=0BCq-=S=XYFLNVhH3d8bHKgd8 z&}W4oX+b5bry#T_fQ*pg2f{ug*}R4di)e4|G{Gk;ac4D`RD6|PPzUS*!db}T>k?8J z@@1J0HVJ~w-r%L6cFkuBzhQaU;taM;cm*UbS}|->B^(p_I!?%`Hu|Q&5Yt9(=%WLP zlc%_4KSnGRG%#!|VQr2>o(-D=IepW8fhoq%y)6r~kW$n#O;{0G?6fTlfD`GqfiiB0 zelAvMU8ITybyQ+xTlPglf}RHU`RTb4M)v*0s0%R5c9a(hS4?HshXIzyv4Mo&^SAap zz^OQ!F~T03AV|q&`4o@IaxMyK0Ev}Ad8<3MxeTADBcRI2*b0XYF_S`9mP1~G*RG}b zbK2ca4j`)=L4fGAfk5nV#{#MmF*}p$jaBg-H{R>tg*cH=48LUfNih}<0A(&|mbD8J z%`a(56mZA8LE9EAonrz?j%rkezi|FT&YJ<%wL=~qoiJ}aUBQ)O&gA-T+H!M(o8Q>90n)OS$ z+Z@cbNZZpgvX9IsOwSZgn28JFoDp?;G6VTA^FO=tl2}R(E_e87bD`H%{55@fP{#dJr(Y=% z<-(7IDU88lN99~ZbtS?*(KQW@$bq;qt{whVT1+$E;*hXH${#@zPOupLXAxRtT~K!k z?}A|?sns-LH*8DFPn4y5Emb(#h-zxvFauGla$IHa2De9hIWbWXQLaFVbMb4yW5fJDLw*}y=17Th4m1xB zift|Sh|*R?)}CKXl5WJs7;-2X zI?pZDMe&$)4GeKV<_)rbAy^TI=Tt?!Bkypy z07LKEDqKcuTD(-V?7LyH3*3#lcn-l$1wMhUSP%0MJt)MB--+mXg56dES4x3g-y419 zQY4R|y%!tCD2Q0FElrqO3IKaxB_3L^XS2@V=ZVhv8B^5dRAB^j0j6B+dZa-M*yittY^?Px{+AV~xgu0CgQcMmZyKpPS5 zIZLuJ{kZbq`YM_s5>8Vnj*VNRj_|9a2(vsfW~iw@ps^voWwt#TPaG;F!T3;(3KVaq!%S|TP>E)ni( z$m}793(w#wshfBtl|W=7Q^>>%H)DWtMKJ%XhWV?(@~@eFy8DRhCVhrjP}aFPwUUs= zI&y@$_|K-)WUnTWCipQ1(pV>O5CzWK*64g%s%`gSnNHomZ#dG<2|8e{d}yR%x#RW| zMzXKkHu&0Ai|4O)>H2&veirSX@m)m;+C zkf@xjY`H8CLKfs@<^#~*G7v3xT{RJEzWmDughMMz!w8egZM^WwAbtz(N_n(Ew18Fd zL=;__WIXw2bG(hSo>1l_HRrxv)S`o8eyM?I^zjA99F9b+bN10<{u5TsqFhw=qt%iI zvf0o@-d1RTBJcTHp_i%ih}yU8IrtBXo=M5tjbk) zvD+{Z1Dsga^=yKrbon6@1B%Xz+Cmi#RW!1=&t3MQ{4q5v$W*jf? z9nc`du-sJTRC*CDpiyJ1+8d_ZIDSghn$Spyr)E6X+1t%92j(FI)x6$W4gW3W zZBALblS7#o_9VnJg)6<_#PFzhre)lmV1Mv45iB@IDE?M{t`30MNH90x7JGNV-vqhR zVG1G7BEF8Lk>^Nz#)x;5=wtjO7Jr(9(_4Hxa0WyD$GN1WNLbKTSE z#`%{u>qpy1ymW8b0l0InbYK){0{cmrbSPTutR9&)^f5gC!KPCZP&rY6+Ysti*wb+H zd#+hG^3~dIl~*SX@Ae%9kSHfm<8%7+^dW-ATlO**bkf6tm(MwG7aO6}fEeI;88thk zD}7f{(`!(SzW5j^XawZIQSrSGRX2prI%~qwDl2+Ftg4!%^H7AkL(nRrt~2JK zYl7*62b5PI2y;N3tUt-4w#H@IgI;VBk<{CQ@TC@T8SZv66N_W;CwMH1Xny2Qx8j=s zqhR?@G=8Utra7e16pdjVi_HRH>LY-yITR;rjSI_bU+7*)(4RbB(xmcDnb6-15r1e1 z1Wa`8Y^^#(WrU9{FO_>hPJ~zs%rW51pxZzjgfby1PSzcGweVZIPt~z&(h8TwEtu&# z#AaBJ4Jc6)Owv4OM_*g}0P)F(m%XX#Xy~?3z||fogktQ~e;w8VXp$x73#49*0p$(G znOZhuD7WxVQwx+Z3a zaeAr`3n(&$>JQ$TEE6VpHPO~Slo+!XwLL6GGN!k6uaFRtMwHo$OX^G^rw!8yU@%@zy0}#X>w8L_Tzg0Lq1q9F3j1Cb91)A4 z)mPQ<@}RWNKWw|Z1fX4TBy56wDi=AX4d)Kk-7?}t%B6;31^r|rud_Sb!{uq#3!8sY zo=oN2=@| z7`D+~JC#yI8d32^kVPZQM4!l>sW3#XImVI_BVf-UZ5t<1Q>PbSOJ*kX^7# zLHT%FWc`Vq8Y-dL7Zw)A+c(&shc`jf+cD=4cLm5Oy#68|~DvvN*UU((2p=Uv|Z$I8heTbACMHw!IZs5z!* z_yb%2)9g7bKq`Ut@YHxO+<%79S5v+k#`UoWkmtfjvX~;StQI^yFvKrUUE46+)q`hM zhHpVz`$AQZFCdo2`V}9BE&(F4sW{2Y2t9Y1J*FE&$pXp%)FtC_jYhdyxa-}UAW@Ve zGPY18)oEmrEzj<TGEE-oe>R|Q5Y?o7%zzKZMrx<|K7{v zHX*}ItukgcbRGl)wZ^3CNU{@oyZYNfCv@8aTVj~5hJt6fv9;XgyA52;rJ+pG} z;GRlild~r(Z1VN*%z znAK^TcX|))ReT*eJzJ3X3^woZx6WivPSc#41DNZ|t&Mv$%~j&l%HYpIcFoOb??HYI zKRSfH>HX^Rf*URj^h6{wLU0X(8kdFI4l#ESBcPug^!B*ZX9%<=aigjFNmsUlt4hob z2edw^G<&skNo^EeIH4oW^8I(xEkj9g)2V9uRb*9-=XpQJdxPAjlHf*z(IdWM$;#?m zgRNge%9{@Ikp&#*GOF`NDs6~Bi)bJ+{ZMC8=`WEb?V02Q-(ZnzZLA)f2FwyT{b|A) zK+MGI{1#7|E6v!EQ%rYoTix8*QH!ZdZ3WmmIYKrAok&;xJ!=5hhpa4)GW3XN-8kwv zm4wTvpZ zI{FPdxToEn5H$yew>KbVZFopb=+oLf(l3ED*#$87iVn7%2PO3fbHrb@oodhk%y3jX zEpNG|e8VHwN=Fd__h=`r)i)t?-Yt{D$C7|`{GSnjE&Iep8+127ps>R;q8=TD9_+x* z9zBig-;~cgBLT9wz#A2Hj?@ucs=kWxbCu4v&k>h5Ck1c6Kq=j|lrr0ek})DGAw&aC zv6d5fD@C=3v34JHBv(}PPrx$o3-AuP+4C2xE?r?B9qf(8G}GTCeuF4kSeDRvrKJgd z>AGpIUF4Q#SKr<&eeRh-Rbw6|O?1z~St?>>W!_@B6l(XHih7PRx! ztWUi=A>P`^%IL0QjTi;}8eyC=xrc}dU3I5F;3dB>{gaNgAOS=|!FBnHyvvA@8$(Jb z%}Z~;j!NCNt0yih?K_cN`2OhRw&&b6u%^>u9jE^dUH5fal`7}?5SN9Kb79fX9_=z} zwX_({8hMR#bcI!Zc)cStc917AGX%cay$x^EmUr^qH7cRfd+jv}a3SNBt&_a_Th}GpA3OA%XpKL5DpeP zPM)m$xIA6qohFlZx=7Mm%tvmUb$1wDW(*<3PFn9+Rq-6R^?%%J9u;ZvI$t^G zAu=ipvgAYj;TWK}My$elyAv+<~IGE{ZE8IUgz}*U2VozC6#h&tJ(#Sn6bMTQA#B;k(S>K%} z05P)D=#zIPfq%zbePw+WisCH0SdN+3#Jp@P#oaQEtGk`Ce|WEsS0bCD3Bb)qX1QMr zL9zCGwG%TO+oieqY$P3`gUR_6hYz-NH!4XdoX~lg8mvsbufveVdzlvLk?8^O6Tbx1 zUhaE}B25bs{)C2(?Kk$K+l%AX{D=$c$AUfc+~TomAl6YXywr?$JZ9qw1$Oa|`Rt_G z!K@b85E9}&DcLZ9=(qvT)EwgA@l>9EmbZzQtN8H!-4*8=Ncx1}5I2(EP?fi}JbwnY z(OP!$6~BJ}@@s~no_)gGku4a%6>$sExmokJ>$993K@IKmN-pEQ@eTCB>BV{!#YN)@ zUumXrNBo*y0Wj?^XD6gt#+WErRY@3WrZ=g+s`BLXJxDQ^jlYZPxkd(7uX=r$r+jG3 zS2H*d+4Y82PL;8b+LkW2gNyp5Q~<#JX$i z>IKrEq@_{j20xN>yTCRUH-^xR#aIXCFf4*Z%H=UAF@kvVjq;|FXkSS5i>G~32CNx zF`!{Hs~a(Lv0S`4gUQTs|TE*!#vEr=@vd#shQ#)rN9CW{NAD*t6|!6=K-b)BTvN zp;;ZrbX>=>47JXj8iZK01PZf}CZGBvF@`Y?Rj1MI$*G2w!Gtn|YTn&jo^7FkRs}CzJFH zo$S{Ddc@^IBXZ6wh;vi*w&CrENhZRHz-%!!fPUZ5E*A7vNzV{72dVFzpwP>S zm5}`+s1xN$I@=BYC4N}bPzT`6%f5`1>^Le|J0DDVs|zz{PO#FCK*ycg7Gs$PvW z%HG{$5qjKf!f2Py*HXT7%R5DLeo~eXWzpKm2lE0=`LkJ4{Z8^-q%FOdOW{yqbDeTc zEengt0!Z=?JFj+ayk^s?f04>{Znp54=L>4W()!VWX<&k#CrOtC-)yAHE$r-V!x=M~ zGJZrDRQbdG2ofTA1NOv0eo&9fTohDFVa>^I=o&0wGkJ6(W1*4ocsHcuyclMtgy3! z4;^0mb6Xu6I!@D~XIP;o;HL;u22DTb&SQ!W*inimB+RaD&?9agAzuW^I~&UiiB2B3 z*0M*0_AR&8W&rQf+h9HaGIo)`9!x$D+@(#Kmh&&`KAQEENNUGRD&j{-5`d@2dhNj; z_5+ps0i{n&?T1+GCs7S;(%tji2)ji+v?+9bBBusi)(!Un-@8~-jmp}iZ^w-dOV)#6 z$B7Mb)GgW!zoi3tVMijotr5}-GySuv&tw@wQx7i6=a&y8UH8bkC}bEm?cJx>k3SYL z!sZMaT&bqH@A7U=FDtQ-yNJuc!xUO-*rSbp%}r3FSdu|5f&(@wDO$R)Q6TJa)a-am zt_aXUW5}HXlU``LKc_@03?Zem1{ou(4Kn2o`t=nM0fmP)ViV1RJ++zgn5g-0E112M zFuBvYj*JjS+IZ}Ugu$HU_% zDk_$=BUL=p!o^6V4eROT_(|tF8(s+E&+XEG;G&5Joo=0^UO672-4b|w0`>Q^Ak0bb z(Gw;9s)uo#?t7F@zDV^V#{(?$W<*bKlUjVQ`8IoJ+7qr6?Tmj~R6; z5wrpgLp0-E`uy3cwQQg}aBG8-t^!ZboMSwXyv|1-3MnZZTw0>!qcTX#spO+KNHHMTh!65GEWkz9 ztwgaVZMP0&wGYJ9ac*Pj$S>^aCHLsrIpOh8xtXE#-qLa?OT803)kz{`jA7TX8~sjh zbWQ@n{1$jI+}$D4BSVIIC&Q8)y;yEWhT9XZZYhaccTp&&Try|7551}KplKQE__o6` zTq6~VjGGqm;q{Xx$@KgWz>=Kap~brb%|4ZlO=}F`ST36~27xga$#XI8w3;-GPL+bz zy%Cb>!ikZ7p=1~XJU;R=kR^(>VifnqS??sBSNbOaDH_)T6hfo)6fR8n~u?)NnNH9YbLOQS>4J@UmhwE*j5-4TTK2n*t}S_CwB^s7VvY-;$R%N za`CEnYm`90nBsu!h%rjf48CN2=)>m4;+Di+ovYVruKO}mPU|7NW`C#J#oL?u?YVfJ z69f@(8`fH9SQuk#M_>K`qgJ@mh9X|f1sC+;~SU<0J1q2QcQLIm~j^L8*y{BRr14!6FBe(pdg=FwD36*hP}bw6}v!{2fd# zVGL%b>;TLPVCHKM+Mpxz@&KOk1}TAqH87QFv@w&>-_t_zqZqP=>xn2o__iY~3-sCE=58#L4@C-T7b3)Y~?$YvMXi*S(5bvD;HaAK6E=ux(F z9e+#EEzFN8D=L*0T<`%yt!1?LHyNUFqgSqhS`jYd1YZjs-?CCYD_bXMg$HLy*@20@ zQ2Nle&dHpXnPi?4JHSZtV4pp$rW_N9UPvbtKUKu;F_HXnA~C_v9CeGUH0c^^dfWem z5TRofqg|+UDz?r`;R3=QbZ{O>V#80 zcy9)CMbR(&yd{*~kQ#V#Mh--{+Wo1cx@taWKO1#|+L=91Zs+DS(i(C%X?& zrT`J8bw+W!t{S= zS&p>CLi0*pdx@bo9Hd#gOOR`gzX4yrcVKpIwlSJ6j3|3OyM!sQJDJa&BPAJNPy=}wccZ1+Q_U(ZmMx@6t&d+Gb*Ag#2ndji*X}hn>B0o zO7?gTw!j%@Z-!s^wFTwBul=gFbVR?W@rZ( zQZjC$&!gnk{=ju^rD-zDTSqziW@rX!y%zA>zRdcd8OS8}KOb5MoKb)}=1yBL9avf%^+-L;x}a;XSE zm>mYb1Z-9`&RVGwYA?2>Z9rfp$;S^DdUuF;0m%@McPd#+twX4K+vFEzygrbhp6>Y zDP8|@`gst%D3ApvG8cfe#NI-@WOA zZT<*Eo=$iRZ+y>7M)XyMU;N@vzp|`I#`AuCHF%6?f}~NRNbR`y%ygNc>^#P_l<{OW@(eT>xRQ zocOK-qwVb!ba1SD$V?%9qx;J6rm;UwxRl;7l-@Y5H<<9m;_Vs_2p6uA39B!G7tG-5 zQ9&2Z15a@-?Ro+_^hC932&+{e0M{VuX6KfS?>xKqbE-GmnJH+bl1`0d+WJA~@t;GO|x*T;v^Ls2y>r??{| zY$0i;uTe$Ytv2g>dMbc}>o4^-@a|mer}jHYvb^DC7uRwe$>-+pmfYJ5g~E+;iR5jqbYq+=#t=_C1^#&VJ8;!&ChUmV z>@jwP>wiS~G6n`~ll01R9wqLvJ8bINX&QkJbPTcRZCMJmK;xaK$2!>J((ukI?fRhnv=wLJPPC!Rfbuo&QA90UxGOI%B-E3}U!&MpIeQr^ z=FY$~L$;8xfR$Lu=njq}@_0JE@8HN{2jaX=vIU+Bn|I)oAs{raI>*h{q2SU$DRQ7f zlrZDBqv@Rq2u@c^TWF;~RR4MH86tOxs8!L0M} z8J(}?!k_jpQ5g*Ld}RLA)G>xL+p1cMup}Mv&?0_|J@HC9VhQWf#?h1`k2Eh%I;t95 zT8*#h6W8o*z}Muv*;pj&ajbE;JnJ{fm3CudHk5BdikU!}bZm@lLwMN?|G;g|cAt0J z31T{|!^5J|!5X*gY zE{~_K6k3-PX4TM8Zo<+!w%UNp=|@e2qhLpS~1d;V%Q0Th>AxE=U9p{ z>#rjAV4hA2r`n0WvD12`mbk6jd})zP4u-*ws=QSbm+TH{>2CyO!~ArxHzTfSq!i18 zDgja_yGocPDUrJ;*q5SDfR?fIQ(1U7uMO;=j;(uK7Y-1!Y(sfk++E50`ntUj7j;VZgwZix^G_%s7|QDjqOt5Y2%Bst=tcf;)vY$s4TN z0h2fMw;J<1ZKrTOAvUI!bN-Utj)gmNt(Q)g1(`Y_T2(@f7Y4N|A$ZU`!pz<>%D0Ps zNu0Yp3wOy6{mDycJ00#xG29EckPl0-PJAgrPob|!v2QcW-0J+;892C}Gsk=*5+Y7G zc(llzs}FtlF<0!_&wh-}<)MQ%m?j-=eK`CwE&LKmWJR0mQ9b4o_?!KU^IR`GieYVp z&JE@ohi!$3UihV9g~R&7$nes#Zjsdv&kca|&V_0|s$_J_^6J(E(c+}ZGGmmIsA`5X z(Zle&R|9oIj-!F8D9UvYi_NuhI>pRzM6Z4c9JjS(Zpu{q@UmT~DGFEKn(NnUGd7w^ zB0X+9EZC(P`N+CFHM9F3ihlN2HPd3c;nnW|(~yj}m1GX?GnS|$?U0Oge;+L|{q_R{ z5pVtsgB&FDvec8tMF$kMGVWzeIoDF{48y=KeN@|{WG?t&Ia-q{;!_0drYhxbTj1gc@W^cWQGo5nYQ8U zBf6HW;2UIqq%CPdS5QiQL)dg_L?<;sK2_LXGvJS0Frh|VLQ;W>TnMl!=~R+xu10JB zcKYlAzv?RDJMntN;UNC0cPkNFc^_0X)i(53c=|%t8Ic;LuAw{p7(?d|2&T=`VDe33 zN@y%$nL&mwkf?)W8kfO}lU#AYApNgHNGr1dfH9MWf$jW$Th}_+zz#3HsVmbWxd0o8z@rw5s|g z>rc+1wqXqtm#{8mgLYeBL6(!gz`6&cJy>qr!PUK-l6IgO888GL(__bI7=-Kt9+*NI zZ386hV9cf%$CGXvRB<6%m=821a`dJIQ;a@hWOSxHI0hQ$4_hP{W{-!ZT9H?DN7y~K zHnOF~+)nGXIhShe#5MGjrfr<;`A}OTk8U~P3pK5DZsNk4kY=tN6O}9~%Iw zOCf}N62uFQcmpp&7-I^Mu!(v$uZB8>12<8o*_>(o{u|Q_UZKEW2Vp}oa_?X|2Li-6 zvK`)I2_K!3%LZppq6&s$V9a7*;(dj3^EhvNqw&r7?@hzs-M~+Kau}310Y;wqUMC-h zwIYkDj^%?kMS6VyWE~A!#Y_*GuN`RJp(VT2a*;)0cuK*83CxYLgH>pYiDL&XlXe-U z8=#bL1}Yl{D_X?+XF+$2C>UdG`19H=hhY=nUYXUi&&9$BmDg zHg{QVQaNcnIY;Va7&o1*iYhBFeVAYvxG|%j!tKSCaG)m}&xw))COP)J$Hbt>6TRjS z+w#Z5orPu)?FC7>H`Sof4pgK4vh(}r!V{MtJ*^95 zq|z5Y>}ii#j1%4Atk2BN-)OK|?Iu{rH95zrOxrJBEo-zign@)MIBrphqaUM;@Ny~E zp_MUgsjE4ur@1@ZMSE)f*D$a9L0{~msM0g`ESt|7E<}!8!~Oc?cHuF4|B?EfykCse zt^8QpE&==B!#l$!-R&IPKLt@_6e%Mf&!{V3{VH6R0(>~XGj*%wMicoi|1l2K6D-pM z@bm<7c!SEi4@;5xaf<3*{Luw83=?zc3|>RjMW>+iTTeG9d|^fmu1rl zW)z___UnoSbcJ(X6SXS}4KPWq-^?>=GC+~ltFK$Hsl(0m0lWG})!HvU`F8G2+laWiXmC+D8yYRz8ReGomGVr1A51)t(KVZ4-gLd2&j``>fOJk{a(|GA2-h&eAquf`< zud4V^M$=n!{H`m1t&ChnJx%J!IQxuvGcTRV$x)rl(`Ty1v@BYekmqzGE+TN5YOEP? z1RW3dcH`Bx*K(uu^{O;enT{scXB^A0T=IzT;BXTZB)CA4RWx5eUOzey)XZW4&2rjRL)nM_S&ZCFk|0WL8$)WKo<&hNI*O_E~?C>Av}5knz7AnXBwjh zwsKCzh7V0eD&s~_U~IUQ5h~EFe%@-WWj2 zu$v^M4MS!_y0V(bBL)`Mbny}eJ-X(#)FRBFpheq7%B)o$3lxflJLQU{ie)nG&GGDZ z9Fwr1gIOXy$Wmvm>&ex}mxYq}K&Z8t6Oj!B^+nrJp8K~#-9IxX)1t;53#Scb2Kq*q z{{L*~ou=TIHzHI`E@NJlo~iy0Oc1H-TBeSj9x`>}8~W&c3A)yl_EeD=hmjJSUO@?W|m@2)w7 zyFT{-zoBZs!5dHLdUOS)#jO(BL1zD^`Q!Vm&v+Zf&@=0<-603ptgDQV1{exWKD^y z@+5WLl-FX8CsM)Lv&!1yMZzeL@mh8rlVKtDpnO;ARJd~#j3*I$YEnmgx55iNIOwyQ zGUt!`?{iw`PLgO}RgN~tM_&+WnMU%< zr)ti-cY@>_-@ z(OXBq2MVSIALtWL`Qp$dS{rFH@Y@%QhLB#8wtzoWfx1ya5Q%t&K3Lr2>!Mx4&EzYt z@?rM81VxLhf8{rlypLWGtOR+Pzv%`l(1?7tglLwxt!QmdAnZ?M&uqRIA*k` z=u7a9Vr;t^gOir##ABssHQAZt3@09x6J&xto5uUEIK1oaLAl{;3;cWI`cy^K85nki zF74M?5!+*!D=2t3d|5Eydn5K$^m*2Ve29`G!GY7XFs63JNLUdgVUJcBc=(aDhR1@r z3U`Kc(XO!}qszgy*SPr#TN$`U5q$P){CBT9c|(3JIJ;P6iaOyDrvvSrrT-Ec+F~K2){y=bh^VfQ6pRfT&*$ zq=?GzPed7p%lXnH;i~rvmNs$2Ub+Y-i_zQ+AQ^Q^8>k*`>dR^u5;`xY^35mQ$)=hx zfy|pGHg~`usc;kcq&`AgU(nu_y}Llq>%>B?qM{Rf8YJzExN}LvPav*6Tc@CTX)~`y zn5QLXJLYAW`83h>GBFd0pu05h1xR&r1YBUjkB#BM3*V`bZVYB|-5tW0J&ywg8 zXTQP!l^N5Fz3n@H@aEev|0y$a{@>{j|IUn}PEPht|D7I5{>^vJ`QN+{v{qec(NBSd zxJaHQEX?Q?bjSb#5)y0zz%Adl-iBo=E;FvwrzTl>#1GrXKY4#pJJ{Ef<(9&Kx@ZGCB8lYTFm@AOLR&ikX6tYFQtZlk+xaD=Z}6$dJzT_g<9 zDs$|T zwz-6K1wq4>)Q3$`F_&A2cOqv8Hr*TrG#U^)wLTZ_MvJUdD{pc?3`yEmegp z#NIYWk}kceT`NZPtK244bW^`<`by>Wf5xC8^EwRGAXBjUu(nXIlwh!TlyL`h@#iqw zJ#xfZ-o&p$eX8#v9tQM*#IAVa#~;&&A7|lH_`qjF=K9jTGRob5x+JR`vj(3kDeocT z#Wk~eJXJeA;DqE+=ur><(%;8<%hP(3s);6#e!I+we}I7(MpH-rKKw5tEK@FGcli$t z1oJ<_K-m9ZmcxHnOF62x&WmCQesl@6;t)WA|G=SWNk!Qjsf=L{5yo?*heoC&LCEsE z9ADzZ23A8aklOj9>1Si|EaLb32O7$|3Gv*4_h)vjZ?>JR_vAc<$>01PWr$oG+ba!; zN@q}<+DjFcS{<@STS#wa2(d(FMiXPOKBx^rPH%C5cYdeHNN;1PWJPz}9RR{JxmD0c zW7b%hXpAsLj>A}Z@H?#n4fUDXJof(7XSaJZ9Q0L+@K8=h%E5M6sICZyDf#3IQcTSX zG+cTRZs^>sjyMIa11)ys(L4cg#rgQ&P{Otxjlv}5Mq|7}P@MUesCs+L?YAAYy;!5vo$ym>?RmfBBCBk&188Ra^!B z?pEwY+Yf`bF1R3k;#Zt8;)O|HmPW%Jh`$1IEP5etHy&ibRab44ciWpTD zTb{u@m=kziJn`>iBzu=08=le32y`~RKsW#^^Tm_C+~D|@;K!-ESW zxGiXX&QkviPTWb@+}(^V_o+fS9RA22jVM(SKX7^PPX;Wp~=tPf9n~(^b${6MlwrraT&drey5ti(RT90qs z0J#01)f13d2L4FCM{5IWDuj!cvn zG=8NDnuOYYb0LRY_}ZU21nBkv)K950LCmPJgk&ObzI{aknf}5tmqhTq{{oL`5|U?8 z|GbMb|ItA4KM&86b`GvC|KITZZ~XoWoo<@Apa{Yr)nWk?My);z37EKIM^Dg!7ejAb z@YHQHF50ucJNoz^toZG}5=; z=}HGMgEzq)gWQ)EXqCVQs0$We^x90+3;HE5>F0gSk4Ek)x`=MTC%)Ih$QDvBx39Bp zN--VP2<*3GXMtg&HZF0aQa4fFmioZgq$me$sVNZ$>up>mkfp@05$ zEUZ#hAgs*JxIxWfagFeX>dM&?zjf`V4Lsd3F9(K0{hlErDY!IVu5qroQykmc@^0zQ zs>xzJ+E{AMO9Zz~(*+kM-_uC{A1e66)Gcbj%Z>u;08U>S&*fE|*2^b-e=UqZ9o|NT zL-Y6vislzoL+3MS#zivon0GE`KVGPZ|JE$F8Edhkys`mI+`E>yZ_U95Y$NeCIoenB z6s%JpY1k_O_`~MtA7YMo;Z6@~-60O+!1)N48%W~T{JqeHr~h*MxHlb?b^Ziw&i@#& z|KAqKzp3*%|6A<3VAlu&M7z4M@Q*Kq;4mtJo&ajOLd60U6)LD+&NPc-a|`vTJ>mEOTyCsM!)}7ItoR=jGw@BSmlapHsr{k8l}O52P%H1z5%}?o z>=HeTqOa2arWMX+JQq+Z?N)i97#VCYZqCyL?>a9~V(5%%)p=F9urHWukP9G$@*50c zyDEi4M#hD=zlW=M&Wy03Ev#LCo@==Cd;6`Zu`4PP&&Q-Q6YuU-i@PBmn`zd@?)xNw z&T@0wvs+ymdR&h0;sOP5DKlM3%E{WbM|oM^)MkG2_t9yYf*HZ?lUTBMKngRhnb&26 zHo1abh!s{@H8P{!y}}EmtRI*2)~P2RG=Q*mVh5jFOtQU>s?Dctbmz&K}5Oq!GqeLFr;a1Tl4Q|w7BSn`IUpI~;d#bY)( zucXCr%7q#JFzh2OB;J97826qy0WiiDuge3X6K4JaGv|j9Re|@wfflk-@9S-r2K<3Y zrQ$)EkBEFPoK~Rm=H)w}!?#wC7AfCejhNJKk|SY8YJBD=2kWW`O`ykG2-oX*e8&~u zGx&uKXQR`m;8q_23ylh{?O|oO34qh7T!2Ynq#=C=!gJe#ugSe-9egiIEwMb6!4@?c^dm=r&@A(cmhrA& zjnsRCR9kxqpGfaC3pu6v`o*{M-;Rf(+SQQIyeG!ToUA8YIgY4QdWl>^o8P_!$o#Kh z*8Qua9*YCyi|ay6U(wf~PcA{XMsST}4Ouze(-54=L37vc(Eqjar~>gakbi)L*#D7{ z`9Cim|BqDl@6u6C(-~PD;ahBVB5Rm!z>UTk6exlQ+XN0d5ON3u1%wH~M#e^BObSoh zy3Q?UI|1Ga{|}!8o~P6iuWB*XSOrH)CC?E2n_|V|P_^aZ1{%YvI?A_|kz_h)L5QG5R4iKx45fvb zhg4(^(ts-pJ@p3MRn+c+LfP}1y=e*;P)^hwr|eM^6rDgBQAGvCU_xKHNi(sKh(svp zRvRGq^SiJ+_TsqV9VoXFhS(Z7MI7JTOXA*OZaV)exCAy<_ z=E@V9g`q|_J@!$T(fGqkCX&d|Ne3Eh?vP}@s*@-l(~XeBMM~#Nepd5a&+FnwH^Q2e zkgG@Y>h(qr`%@Dl;@sTC0>)-msB}Uan0@aZ6iV4luh=Y@vbOHc=r-Nec4;~yPHl*; zX3Avi&SBYmG^(@EVT5{fjE&W-Z0bcRX*=>dZBNWZr9C0D&^e^-7$b9$jLMUTEm!!* zrzb`j@>T(Zm%n)-{XhY1F*I6*V<8FT_QY=M;}6Y*Z6OSVU?b{KvRsM_|~r= zFC;K+AnqiBc?)-?!4ZXlvn)Pv1^duG75mf(gZkDUm_bmz^S88MtUh5MFygsutim^k zHJYsQe8-|$@vBtn@wU}&BIWgvor2ltx=E8E{#qDr@9DV_b=WJkJ^i?KC@thzY32ER zoSIQ(Q2LoIv)IKRqSVX&R$)ti(fd z?!r_TV||%f$DaFdRu!5%EY*@(wO-+C#nQz`Jr7-#wpD|VuHO#_Xh)Yj@8L%u$LIlE zPTK6HdVFlLf}~tVPE`bvJA{)4_`H|kXC*KMu_w1Yr~$$3=cWA1Oo_t97bWn&LI+u} zgwt7mXI`H1E_Iqi2I%vABrhq#KQ4y#Lp~-XKhipai2su33Ox)aF7a!G^jLr<0p-Of z_r<^DtFbQZK)pxQ5^}&SxFZdg4>{a76_HS(3a_Oiz8lL_nvgvVOU+liU7G&G)O_bA zwDt@^gD3f;msVwuBTt)C61-q)jT<$RUUQU+pq{iydVhxE;`v%s!jDO+lBn!AYl#o;M{RhlOX*Fc1;3)LaieeMd0DSiWvJqF~#FsCg zmz6)5mEyKFBMmo`i>=>Sm8;>$g09utH=xn?GNKj2R=m?xFUCZ|uv;u-$0IvXs<_@U zznCFfk2GG;K2j~J%khb>C0Q>)kt9p^jGG66@2!RG1qjSo9xp^y@j3Y*!oNI26$Zuo zY^HyRNcFl@EtfPjK?BKN+-*J0Hq1XkJOOUPI}|mv0s3ZhuFBSd9dc}sQ}0w`IzO1z z%1{|30fN~B@?D{)n5EMER%@RAdsGG!YITwezOB}ig|DJ1VWMKW+rmZ}3{h4i80L!> zkrb9GnmpT4BCzM=m+vqGpTXc&JU&_VrXbHu6!5#cXjzv`HvfIv&I2Eq4q^pIX6Zrk z-O0T=qXKfbT3272Rg~5P~k<6N8s*^c4gA)kXlQ%TO>=y7l>9!^-6%Ds{?UWMh2W6Rg(oA$;zHO z(G`GJix#8#_;f{^#0c#0F)4k3=Ged(^kJ;>W+y2Sa(SL-;bYXGM^VN2n{#dIR;H|#)~Z0E(2#l+BbcwM zEij2@;;0>;N`zjsxV?B)-Nfu_-b%Z38|eQ1OAjRPHU&g#PQ>>BFc~B!%d&%HAHiqU zO#=2cv%bATD{Ff|_K|awee<&2$H{!7=YRME|BPU!viq^YlRZK=iVy2;6&c|lKs~Dy zg?pots01S*X9XaDsp9klq_)C@g6WOLJCU;G-7x61|a2H@ia$ zNcaCNbTV3!khj}$o?T&XW8Ey2$?#1(dksjYu?z^4+u8?un5df2rixRLCWz*-NDWfPTE`|iZf}C zi1ws`n%`>Lr504c=z-znFK7aBq~e=v&<1ph8lDO3#m;p-93C>=OQ~1hE)kk%28J*0 z7W_nqNwdbk{iuMgP zpc=7kIAAW)+q)Ji+pi88U=Ka?`MFA#MyIYt!7i^jZ5OQc;WlNruXu%y&Qn_U}1 zSJSV%%Px!E6XT0n4K|!d9l3|ii>a|{$6{u=ge5G(@fIb}x04C(I~GW5Sr%fX$1Opm zBc2t(p5{G#=G%pHl`--{GNxrcADrH$S+QifR`L^2(i}9cQtUlD0~{ z3yHmqquIe&U#3z4QNQSasf&gWB6HfsS4lNs?$ufsOV5*ASd}{v0$#B3aH*LpzSB9H zA2gxT8>{eS9cLM84EYY95*R~Oq3x~-Qqk7#B!9T&ou?H{G~wMbh~bX07_{5Epff$ zaIRV*Lyo9D(3NL0A@Mo_9tm?Q;kMmX-1gtA#3{UhR_WVmp{j>f(AvU@FuW4M4zL!% zjN!;!19Qa$1=KVJUGz1ssLV0hrtm2v(s|;X1g(Z8P^j=uXASLxi&@AJpuTgSCD6I$ zzewy7cUYG7gc|i-YBXiQ)xnmJWqKbUdx5`?MVM+`9~`GkSi>kx&}|^}gi~A^3&V-; z-sLJ<4SVS^s|nu;b+_>qUHJuJ`9z8Niq8Gzc;5rh|B`mzq587=#{BXT#%I|jin}=# z`upqTj+kzbygMe(5w=f3-G6wr_<`5YSoJlhWeaAzc+Ov>N0c z1eP!3Q*Od#s-#`AD0gOi`2X7Yfv^n!h5jioQ2%3jLG?cj!2fQlQ1|pvQAhpWp~*10 zmmnkju@aV|B&M1E2CA_U0uGp?0X4FUWW!~mPXdg?^k$IEQAOsfY-*LXuBo1dYM~cT z3FH&lwXC#Kt*q3jIkvf0T==itbibI9A(PScy?xF2zU(;9e$8<1_MFT(|K;|Bl4IHH z)9Z%qigJK`4u7o&$4_vp3*YR1rQjpeZ;NUd9?Bu&qdTBO%tN@7MEnrhqCHS1y1rA| zqSpW5eQichS)=9NCO`N#;`t38b%%iWyvMAkL+4EAqc-#l(@&%s?`e;b-faKQh$kS( z=-S!n_<`vKwBfcmoL=fhdia&8mxwAX@=g`!*$6Q=v5r`eBmv2BDash*?tnvXEV4{A z`*AP1{2J|!6<*H!))jP7%eY{sKQ(*@E0&z?46%nDF;=vsNZeI8U<1i?69sbU&`T$v zZ8RLVZgH^SI~b@$RM1jhK^|lvLC)9PD5U@#MQ5dxl>ue#Iu1Upi16?GJ$8DlOG0b|7#OJ9(Ajzl z;thBcsOPJb9|@Xv>pbp}_8vau$#DC&tY=DtI=Q6^7;>9!Yh6BC5R`Pma`Es^K*Am$ zr=Hz%IqWeUD6V7woGetdH?y4wh1rdQ0cmrh6k20%N5 z!Z1AB45}6JPj}3ARx~6DO>FRdc`Qw`Wb6;@3#D9zWk%8|4o2muC^HxVeYi5~XKP8o z;>_e^u`LQhtW*nr9O86T+N-PR8R}OXdcew5cb0S`d7Ux9A6MSMo(BdiYE`_6!4w=B z(Ijq4f^6r+kvt8>lNnud5d#$732ai1bw$rfgLY;h@ve!gf;sg6bjp4f$YCqIrwd~> zpvhNbriegxo~77d=6BSY2*aDrI1N*jqz04llQra6+zHQIR9egt6+19iZHaQ1R`y%O zih2$wZ!!Q4LiR;#3|%vyNR7(Ohaj|#$JVj}ZN3loIL>g8U?(}DH=jWcX40Ov*|Vf& zQ&TbV%D&C1F>rd`da)0K#pvZ!>2=@A8;Yo5N(JwN1Y-#^0Jk$QGV3+Z=z21nebJ-vgIn? z;qqJn+*NtPZai{g%lre>RX>sHe%ejK^$(-L%~e0a`c&_J`OF3FR=o1+ zsoa5oDh}t}CW8B`F@_VO)*Zsae-|Fqy=H;?tKX4*7aZgPed%)uf5*x8M*1J}^BAEY z!mej~Yib8=46o8gWpo%OACvqY`7jiypw(kuq z`5MyV6-0aP5U6sF(zFGu9OoE7#x^3=M4SS5Zoy^*VL>`9#?oqM(0(M^ED$HKu%j+T zU%XrJG6R|F`1BOKoPotfy&>#}C@vBymwW|yXiv>uebsn>i_sORcPvNlV<5M6quRc4 zpqks@+VQ3-lC7Mb>;|AxAi%^v{HdCSh0Nod59Y6{?MT_IcKoAEF<6#ys7!p7Z*>aX zPyka`H7Xx8tiE0mp1ug}b9YO7c4BN^t!a0axV0f7V%YV5yY+{`N=225-#u9qm97CM zB;)G}w>m0?^`x;Pm0khJ)?Zu6>j&`$Ge&8wgu&!+Ga zBsp#W7(P;%8%p#+R(_!1Ei%!;Y6Fo&ejp_{3s~N}E2lRz)-DaAEgVSSS-eeL#4uxF zcm9f+LOHhKE_q**Eu_~JSMmqtxhGku;7q1iJfEuWOs-gsN>X5zot}?Mrdas4M!)QX zb8E>Ln`|kCbfg7^+|o%x)r?$z$rmX}sWZ|rPF$`ucdX-x@Nh29APTT-Pi?Q%A$Q~LrJ7Z6_^VAnHzs2ZsJ4soQ;*w&0{@E!1lJwpL~f=~ zYdXXtR+Yf{@GyJI<6_9S69c*yScNbkB=Y+AwnrnHP$xz^V^??TR*wddGj+>ncBW@` z{LAbR(rgc*$Upr~p6R&^^XmljOWop*b?bK%jz2DYW2l`P%GQu#JT_<|>P&AE@7p<< zHp<4j!1T0;SGwBn@D!J2U1qx-(GGh=3po3OwBT=!X&hu;eIJ``7_qm4=q%7dutm4KxP1DT`;|o&2$7yoqG#RJH^)%X1wge$l{g)`U zIM%ipcsBSUWeh=K_Gp_Ufr@O$*FA2>JUCB^0C+ z+)J*`gR&i=;cBiaK3$CUw7;0kYwM&9{k(;tpwYUTrPNER2%MU>)E{GC>n0BB%#H(q zo9`g*_#=^cLZNtKDS0B*4+yZvfB@Zd8>B%U2{2o7FnXm1;C2NqP$~(EH^j%3>ESQP zJyCr*m~1u0u?#gNutLtI^$bzP!PEg|KP^9Gx3}gUnnpM0iD|92w1C{xSwHo45M>1LXZE>xpVO#3^>N=YWp z?f*sDJ4RU+F4>}$nU%I}+qP9{qtZKV+qUgW+qP}nc2-(9`<%Y_z1ySDdHwbnYwz`E z{aNwFju{a%X0-p3#wP-*ydxMihrIa*PqqX43)TVh$2TkE-&Kkj{)bRb*v8Vy%KAS6 zor;zVvI@$ltV5a^mjI-^l$L1=xtVmXJSen;83^c{n4}pjfm6I}LTSRU)L)uVTY|UV zKDV>zUcHSBrqHSix{kE?Ud!M

5NnGuZl-b#+YJd~X+>(=LyIqZl8b7I<{7n^t&z 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 Date: Tue, 24 Dec 2013 18:13:33 +0800 Subject: [PATCH 4/4] code submit --- .../memcached/ByteBufArrayInputStream.java | 136 + .../memcached/ContextObjectInputStream.java | 44 + .../com/meetup/memcached/ErrorHandler.java | 72 + .../com/meetup/memcached/LineInputStream.java | 43 + .../java/com/meetup/memcached/Logger.java | 202 ++ .../com/meetup/memcached/MemcachedClient.java | 2316 +++++++++++++++++ .../com/meetup/memcached/NativeHandler.java | 443 ++++ .../meetup/memcached/NestedIOException.java | 44 + .../java/com/meetup/memcached/SockIOPool.java | 1903 ++++++++++++++ .../meetup/memcached/test/MemcachedBench.java | 99 + .../meetup/memcached/test/MemcachedTest.java | 151 ++ .../meetup/memcached/test/TestMemcached.java | 63 + .../com/meetup/memcached/test/UnitTests.java | 403 +++ 13 files changed, 5919 insertions(+) create mode 100644 src/main/java/com/meetup/memcached/ByteBufArrayInputStream.java create mode 100644 src/main/java/com/meetup/memcached/ContextObjectInputStream.java create mode 100644 src/main/java/com/meetup/memcached/ErrorHandler.java create mode 100644 src/main/java/com/meetup/memcached/LineInputStream.java create mode 100644 src/main/java/com/meetup/memcached/Logger.java create mode 100644 src/main/java/com/meetup/memcached/MemcachedClient.java create mode 100644 src/main/java/com/meetup/memcached/NativeHandler.java create mode 100644 src/main/java/com/meetup/memcached/NestedIOException.java create mode 100644 src/main/java/com/meetup/memcached/SockIOPool.java create mode 100644 src/main/java/com/meetup/memcached/test/MemcachedBench.java create mode 100644 src/main/java/com/meetup/memcached/test/MemcachedTest.java create mode 100644 src/main/java/com/meetup/memcached/test/TestMemcached.java create mode 100644 src/main/java/com/meetup/memcached/test/UnitTests.java diff --git a/src/main/java/com/meetup/memcached/ByteBufArrayInputStream.java b/src/main/java/com/meetup/memcached/ByteBufArrayInputStream.java new file mode 100644 index 0000000..c12caed --- /dev/null +++ b/src/main/java/com/meetup/memcached/ByteBufArrayInputStream.java @@ -0,0 +1,136 @@ +/** + * 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 + */ +package com.meetup.memcached; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +public final class ByteBufArrayInputStream extends InputStream implements LineInputStream { + private ByteBuffer[] bufs; + private int currentBuf = 0; + + public ByteBufArrayInputStream( List bufs ) throws Exception { + this( bufs.toArray( new ByteBuffer[] {} ) ); + } + + public ByteBufArrayInputStream( ByteBuffer[] bufs ) throws Exception { + if ( bufs == null || bufs.length == 0 ) + throw new Exception( "buffer is empty" ); + + this.bufs = bufs; + for ( ByteBuffer b : bufs ) + b.flip(); + } + + public int read() { + do { + if ( bufs[currentBuf].hasRemaining() ) + return bufs[currentBuf].get(); + currentBuf++; + } + while ( currentBuf < bufs.length ); + + currentBuf--; + return -1; + } + + public int read( byte[] buf ) { + int len = buf.length; + int bufPos = 0; + do { + if ( bufs[currentBuf].hasRemaining() ) { + int n = Math.min( bufs[currentBuf].remaining(), len-bufPos ); + bufs[currentBuf].get( buf, bufPos, n ); + bufPos += n; + } + currentBuf++; + } + while ( currentBuf < bufs.length && bufPos < len ); + + currentBuf--; + + if ( bufPos > 0 || ( bufPos == 0 && len == 0 ) ) + return bufPos; + else + return -1; + } + + public String readLine() throws IOException { + byte[] b = new byte[1]; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + boolean eol = false; + + while ( read( b, 0, 1 ) != -1 ) { + if ( b[0] == 13 ) { + eol = true; + } + else { + if ( eol ) { + if ( b[0] == 10 ) + break; + eol = false; + } + } + + // cast byte into char array + bos.write( b, 0, 1 ); + } + + if ( bos == null || bos.size() <= 0 ) { + throw new IOException( "++++ Stream appears to be dead, so closing it down" ); + } + + // else return the string + return bos.toString().trim(); + } + + public void clearEOL() throws IOException { + byte[] b = new byte[1]; + boolean eol = false; + while ( read( b, 0, 1 ) != -1 ) { + + // only stop when we see + // \r (13) followed by \n (10) + if ( b[0] == 13 ) { + eol = true; + continue; + } + + if ( eol ) { + if ( b[0] == 10 ) + break; + eol = false; + } + } + } + + public String toString() { + StringBuilder sb = new StringBuilder( "ByteBufArrayIS: " ); + sb.append( bufs.length ).append( " bufs of sizes: \n" ); + + for ( int i=0; i < bufs.length; i++ ) { + sb.append( " " ) + .append (i ).append( ": " ).append( bufs[i] ).append( "\n" ); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/meetup/memcached/ContextObjectInputStream.java b/src/main/java/com/meetup/memcached/ContextObjectInputStream.java new file mode 100644 index 0000000..daec86c --- /dev/null +++ b/src/main/java/com/meetup/memcached/ContextObjectInputStream.java @@ -0,0 +1,44 @@ +/** + * 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. + * + * Adds the ability for the MemCached client to be initialized + * with a custom class loader. This will allow for the + * deserialization of classes that are not visible to the system + * class loader. + * + * @author Vin Chawla + */ +package com.meetup.memcached; + +import java.util.*; +import java.util.zip.*; +import java.io.*; + +public class ContextObjectInputStream extends ObjectInputStream { + + ClassLoader mLoader; + + public ContextObjectInputStream( InputStream in, ClassLoader loader ) throws IOException, SecurityException { + super( in ); + mLoader = loader; + } + + protected Class resolveClass( ObjectStreamClass v ) throws IOException, ClassNotFoundException { + if ( mLoader == null ) + return super.resolveClass( v ); + else + return Class.forName( v.getName(), true, mLoader ); + } +} diff --git a/src/main/java/com/meetup/memcached/ErrorHandler.java b/src/main/java/com/meetup/memcached/ErrorHandler.java new file mode 100644 index 0000000..0e84290 --- /dev/null +++ b/src/main/java/com/meetup/memcached/ErrorHandler.java @@ -0,0 +1,72 @@ +/** + * 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. + * + * This is an interface implemented by classes that want to receive callbacks + * in the event of an error in {@link MemcachedClient}. The implementor can do + * things like flush caches or perform additioonal logging. + * + * @author Dan Zivkovic + */ +package com.meetup.memcached; + +public interface ErrorHandler { + + /** + * Called for errors thrown during initialization. + */ + public void handleErrorOnInit( final MemcachedClient client , + final Throwable error ); + + /** + * Called for errors thrown during {@link MemcachedClient#get(String)} and related methods. + */ + public void handleErrorOnGet( final MemcachedClient client , + final Throwable error , + final String cacheKey ); + + /** + * Called for errors thrown during {@link MemcachedClient#getMulti(String)} and related methods. + */ + public void handleErrorOnGet( final MemcachedClient client , + final Throwable error , + final String[] cacheKeys ); + + /** + * Called for errors thrown during {@link MemcachedClient#set(String,Object)} and related methods. + */ + public void handleErrorOnSet( final MemcachedClient client , + final Throwable error , + final String cacheKey ); + + /** + * Called for errors thrown during {@link MemcachedClient#delete(String)} and related methods. + */ + public void handleErrorOnDelete( final MemcachedClient client , + final Throwable error , + final String cacheKey ); + + /** + * Called for errors thrown during {@link MemcachedClient#flushAll()} and related methods. + */ + public void handleErrorOnFlush( final MemcachedClient client , + final Throwable error ); + + /** + * Called for errors thrown during {@link MemcachedClient#stats()} and related methods. + */ + public void handleErrorOnStats( final MemcachedClient client , + final Throwable error ); + +} // interface diff --git a/src/main/java/com/meetup/memcached/LineInputStream.java b/src/main/java/com/meetup/memcached/LineInputStream.java new file mode 100644 index 0000000..3fee794 --- /dev/null +++ b/src/main/java/com/meetup/memcached/LineInputStream.java @@ -0,0 +1,43 @@ +/** + * 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 + */ +package com.meetup.memcached; + +import java.io.IOException; + +public interface LineInputStream { + + /** + * Read everything up to the next end-of-line. Does + * not include the end of line, though it is consumed + * from the input. + * @return All next up to the next end of line. + */ + public String readLine() throws IOException; + + /** + * Read everything up to and including the end of line. + */ + public void clearEOL() throws IOException; + + /** + * Read some bytes. + * @param buf The buffer into which read. + * @return The number of bytes actually read, or -1 if none could be read. + */ + public int read( byte[] buf ) throws IOException; +} diff --git a/src/main/java/com/meetup/memcached/Logger.java b/src/main/java/com/meetup/memcached/Logger.java new file mode 100644 index 0000000..f5300fd --- /dev/null +++ b/src/main/java/com/meetup/memcached/Logger.java @@ -0,0 +1,202 @@ +/** + * 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 + */ +package com.meetup.memcached; + +import java.util.*; + +/** + * This is a generic logger class for use in logging. + * + * This can easily be swapped out for any other logging package in the main code. + * For now, this is just a quick and dirty logger which will allow you to specify + * log levels, but only wraps system.out.println. + * + * @author Greg Whalin + * @version 1.5 + */ +public class Logger { + + public static final int LEVEL_DEBUG = 0; + public static final int LEVEL_INFO = 1; + public static final int LEVEL_WARN = 2; + public static final int LEVEL_ERROR = 3; + public static final int LEVEL_FATAL = 4; + + private static Map loggers = + new HashMap(); + + private String name; + private int level; + private boolean initialized = false; + + public void setLevel( int level ) { this.level = level; } + public int getLevel() { return this.level; } + + protected Logger( String name, int level ) { + this.name = name; + this.level = level; + this.initialized = true; + } + + protected Logger( String name ) { + this.name = name; + this.level = LEVEL_INFO; + this.initialized = true; + } + + /** + * Gets a Logger obj for given name and level. + * + * @param name + * @param level + * @return + */ + public static synchronized Logger getLogger( String name, int level ) { + Logger log = getLogger( name ); + if ( log.getLevel() != level ) + log.setLevel( level ); + + return log; + } + + /** + * Gets a Logger obj for given name + * and sets default level. + * + * @param name + * @return + */ + public static synchronized Logger getLogger( String name ) { + + Logger log = null; + if ( loggers.containsKey( name ) ) { + log = loggers.get( name ); + } + else { + log = new Logger( name ); + loggers.put( name, log ); + } + + return log; + } + + /** + * logs mesg to std out and prints stack trace if exception passed in + * + * @param mesg + * @param ex + */ + private void log( String mesg, Throwable ex ) { + System.out.println( name + " " + new Date() + " - " + mesg ); + if ( ex != null ) + ex.printStackTrace( System.out ); + } + + /** + * logs a debug mesg + * + * @param mesg + * @param ex + */ + public void debug( String mesg, Throwable ex ) { + if ( this.level > LEVEL_DEBUG ) + return; + + log( mesg, ex ); + } + + public void debug( String mesg ) { + debug( mesg, null ); + } + + public boolean isDebugEnabled() { + return this.level <= LEVEL_DEBUG; + } + + /** + * logs info mesg + * + * @param mesg + * @param ex + */ + public void info( String mesg, Throwable ex ) { + if ( this.level > LEVEL_INFO ) + return; + + log( mesg, ex ); + } + + public void info( String mesg ) { + info( mesg, null ); + } + + public boolean isInfoEnabled() { + return this.level <= LEVEL_INFO; + } + + /** + * logs warn mesg + * + * @param mesg + * @param ex + */ + public void warn( String mesg, Throwable ex ) { + if ( this.level > LEVEL_WARN ) + return; + + log( mesg, ex ); + } + + public void warn( String mesg ) { + warn( mesg, null ); + } + + /** + * logs error mesg + * + * @param mesg + * @param ex + */ + public void error( String mesg, Throwable ex ) { + if ( this.level > LEVEL_ERROR ) + return; + + log( mesg, ex ); + } + + public void error( String mesg ) { + error( mesg, null ); + } + + /** + * logs fatal mesg + * + * @param mesg + * @param ex + */ + public void fatal( String mesg, Throwable ex ) { + if ( this.level > LEVEL_FATAL ) + return; + + log( mesg, ex ); + } + + public void fatal( String mesg ) { + fatal( mesg, null ); + } +} diff --git a/src/main/java/com/meetup/memcached/MemcachedClient.java b/src/main/java/com/meetup/memcached/MemcachedClient.java new file mode 100644 index 0000000..c17c1fa --- /dev/null +++ b/src/main/java/com/meetup/memcached/MemcachedClient.java @@ -0,0 +1,2316 @@ +/** + * 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 + */ +package com.meetup.memcached; + +import java.util.*; +import java.util.zip.*; +import java.nio.*; +import java.net.InetAddress; +import java.nio.charset.*; +import java.nio.channels.*; +import java.nio.channels.spi.*; +import java.io.*; +import java.net.URLEncoder; + +import org.apache.log4j.Logger; + +/** + * This is a Memcached client for the Java platform available from + * http://www.danga.com/memcached/. + *
+ * Supports setting, adding, replacing, deleting compressed/uncompressed and
+ * serialized (can be stored as string if object is native class) objects to memcached.
+ *
+ * Now pulls SockIO objects from SockIOPool, which is a connection pool. The server failover
+ * has also been moved into the SockIOPool class.
+ * This pool needs to be initialized prior to the client working. See javadocs from SockIOPool.
+ *
+ * Some examples of use follow.
+ *

To create cache client object and set params:

+ *
 
+ *	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);
+ * 
+ *

To store an object:

+ *
+ *	MemcachedClient mc = new MemcachedClient();
+ *	String key   = "cacheKey1";	
+ *	Object value = SomeClass.getObject();	
+ *	mc.set(key, value);
+ * 
+ *

To store an object using a custom server hashCode:

+ *
+ *	MemcachedClient mc = new MemcachedClient();
+ *	String key   = "cacheKey1";	
+ *	Object value = SomeClass.getObject();	
+ *	Integer hash = new Integer(45);	
+ *	mc.set(key, value, hash);
+ * 
+ * The set method shown above will always set the object in the cache.
+ * The add and replace methods do the same, but with a slight difference.
+ *
    + *
  • add -- will store the object only if the server does not have an entry for this key
  • + *
  • replace -- will store the object only if the server already has an entry for this key
  • + *
+ *

To delete a cache entry:

+ *
+ *	MemcachedClient mc = new MemcachedClient();
+ *	String key   = "cacheKey1";	
+ *	mc.delete(key);
+ * 
+ *

To delete a cache entry using a custom hash code:

+ *
+ *	MemcachedClient mc = new MemcachedClient();
+ *	String key   = "cacheKey1";	
+ *	Integer hash = new Integer(45);	
+ *	mc.delete(key, hashCode);
+ * 
+ *

To store a counter and then increment or decrement that counter:

+ *
+ *	MemcachedClient mc = new MemcachedClient();
+ *	String key   = "counterKey";	
+ *	mc.storeCounter(key, new Integer(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));	
+ * 
+ *

To store a counter and then increment or decrement that counter with custom hash:

+ *
+ *	MemcachedClient mc = new MemcachedClient();
+ *	String key   = "counterKey";	
+ *	Integer hash = new Integer(45);	
+ *	mc.storeCounter(key, new Integer(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));	
+ * 
+ *

To retrieve an object from the cache:

+ *
+ *	MemcachedClient mc = new MemcachedClient();
+ *	String key   = "key";	
+ *	Object value = mc.get(key);	
+ * 
+ *

To retrieve an object from the cache with custom hash:

+ *
+ *	MemcachedClient mc = new MemcachedClient();
+ *	String key   = "key";	
+ *	Integer hash = new Integer(45);	
+ *	Object value = mc.get(key, hash);	
+ * 
+ *

To retrieve an multiple objects from the cache

+ *
+ *	MemcachedClient mc = new MemcachedClient();
+ *	String[] keys      = { "key", "key1", "key2" };
+ *	Map<Object> values = mc.getMulti(keys);
+ * 
+ *

To retrieve an multiple objects from the cache with custom hashing

+ *
+ *	MemcachedClient mc = new MemcachedClient();
+ *	String[] keys      = { "key", "key1", "key2" };
+ *	Integer[] hashes   = { new Integer(45), new Integer(32), new Integer(44) };
+ *	Map<Object> values = mc.getMulti(keys, hashes);
+ * 
+ *

To flush all items in server(s)

+ *
+ *	MemcachedClient mc = new MemcachedClient();
+ *	mc.flushAll();
+ * 
+ *

To get stats from server(s)

+ *
+ *	MemcachedClient mc = new MemcachedClient();
+ *	Map stats = mc.stats();
+ * 
+ * + * @author greg whalin + * @author Richard 'toast' Russo + * @author Kevin Burton + * @author Robert Watts + * @author Vin Chawla + * @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 = "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
+ * longer than the threshold length set with setCompressThreshold(int)
+ *
+ * The default is that compression is enabled.
+ *
+ * Even if compression is disabled, compressed data will be automatically
+ * decompressed. + * + * @param compressEnable true to enable compression, false 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 true, 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 true, 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.
+ * Keep in mind {@link #add(String, Object) add} and {@link #replace(String, Object) replace}
+ * will fail when used with the same key will fail, until the server reaches the
+ * specified time. However, {@link #set(String, Object) set} will succeed,
+ * 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 true, 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
+ * deleted, the specified value will not be stored.
+ * The server will automatically delete the value when the expiration time has been reached.
+ *
+ * If compression is enabled, and the data is longer than the compression threshold
+ * the data will be stored in compressed form.
+ *
+ * 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.error( "trying to store a null value to cache" ); + 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(); + gos.close(); + + // 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, new Long( 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, new Long( 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, new Long( 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
+ * underflow. In the event of underflow, the result will be zero. Because
+ * Java lacks unsigned types, the value is returned as a 64-bit integer.
+ * The server will only decrement a value if it already exists;
+ * 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
+ * be decompressed or serialized, as appropriate. (Inclusive or)
+ *
+ * Non-serialized data will be returned as a string, so explicit conversion to
+ * numeric types will be necessary, if desired
+ * + * @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
+ * be decompressed or serialized, as appropriate. (Inclusive or)
+ *
+ * Non-serialized data will be returned as a string, so explicit conversion to
+ * numeric types will be necessary, if desired
+ * + * @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
+ * be decompressed or serialized, as appropriate. (Inclusive or)
+ *
+ * Non-serialized data will be returned as a string, so explicit conversion to
+ * numeric types will be necessary, if desired
+ * + * @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 + " -- " + e.getMessage() ); + 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 ( Exception e ) { + if ( errorHandler != null ) + errorHandler.handleErrorOnGet( this, e, key ); + + o = null; + log.error( "++++ Exception thrown while trying to deserialize for key: " + key + " -- " + e.getMessage() ); + } + } + } + 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 + " -- " + e.getMessage() ); + + 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
+ * is more efficient.
+ * + * @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
+ * is more efficient.
+ * + * @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
+ * is more efficient.
+ * + * @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 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
+ * is more efficient.
+ * + * @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 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
+ * is more efficient.
+ * + * @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 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
+ * is more efficient.
+ * + * @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 getMulti( String[] keys, Integer[] hashCodes, boolean asString ) { + + if ( keys == null || keys.length == 0 ) { + log.error( "missing keys for getMulti()" ); + return null; + } + + Map cmdMap = + new HashMap(); + + 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 ret = + new HashMap( 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
+ * 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 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 + " -- " + e.getMessage() ); + 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.getMessage() ); + 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 ( InvalidClassException e ) { + /* Errors de-serializing are to be expected in the case of a + * long running server that spans client restarts with updated + * classes. + */ + // if we have an errorHandler, use its hook + if ( errorHandler != null ) + errorHandler.handleErrorOnGet( this, e, key ); + + o = null; + log.error( "++++ InvalidClassException thrown while trying to deserialize for key: " + key + " -- " + e.getMessage() ); + } + catch ( ClassNotFoundException e ) { + + // if we have an errorHandler, use its hook + if ( errorHandler != null ) + errorHandler.handleErrorOnGet( this, e, key ); + + o = null; + log.error( "++++ ClassNotFoundException thrown while trying to deserialize for key: " + key + " -- " + e.getMessage() ); + } + } + + // store the object into the cache + if ( o != null ) + 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, "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 statsMaps = + new HashMap(); + + 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 stats = new HashMap(); + + // 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 incoming = new ArrayList(); + 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 sockKeys, String[] keys, Map 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 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 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 { + if ( selector != null ) + 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; + } + } + } + } +} diff --git a/src/main/java/com/meetup/memcached/NativeHandler.java b/src/main/java/com/meetup/memcached/NativeHandler.java new file mode 100644 index 0000000..7f9a87e --- /dev/null +++ b/src/main/java/com/meetup/memcached/NativeHandler.java @@ -0,0 +1,443 @@ +/** + * 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 + */ +package com.meetup.memcached; + +import java.util.Date; +import org.apache.log4j.Logger; + +/** + * Handle encoding standard Java types directly which can result in significant + * memory savings: + * + * Currently the Memcached driver for Java supports the setSerialize() option. + * This can increase performance in some situations but has a few issues: + * + * Code that performs class casting will throw ClassCastExceptions when + * setSerialize is enabled. For example: + * + * mc.set( "foo", new Integer( 1 ) ); Integer output = (Integer)mc.get("foo"); + * + * Will work just file when setSerialize is true but when its false will just throw + * a ClassCastException. + * + * Also internally it doesn't support Boolean and since toString is called wastes a + * lot of memory and causes additional performance issue. For example an Integer + * can take anywhere from 1 byte to 10 bytes. + * + * Due to the way the memcached slab allocator works it seems like a LOT of wasted + * memory to store primitive types as serialized objects (from a performance and + * memory perspective). In our applications we have millions of small objects and + * wasted memory would become a big problem. + * + * For example a Serialized Boolean takes 47 bytes which means it will fit into the + * 64byte LRU. Using 1 byte means it will fit into the 8 byte LRU thus saving 8x + * the memory. This also saves the CPU performance since we don't have to + * serialize bytes back and forth and we can compute the byte[] value directly. + * + * One problem would be when the user calls get() because doing so would require + * the app to know the type of the object stored as a bytearray inside memcached + * (since the user will probably cast). + * + * If we assume the basic types are interned we could use the first byte as the + * type with the remaining bytes as the value. Then on get() we could read the + * first byte to determine the type and then construct the correct object for it. + * This would prevent the ClassCastException I talked about above. + * + * We could remove the setSerialize() option and just assume that standard VM types + * are always internd in this manner. + * + * mc.set( "foo", new Boolean.TRUE ); Boolean b = (Boolean)mc.get( "foo" ); + * + * And the type casts would work because internally we would create a new Boolean + * to return back to the client. + * + * This would reduce memory footprint and allow for a virtual implementation of the + * Externalizable interface which is much faster than Serialzation. + * + * Currently the memory improvements would be: + * + * java.lang.Boolean - 8x performance improvement (now just two bytes) + * java.lang.Integer - 16x performance improvement (now just 5 bytes) + * + * Most of the other primitive types would benefit from this optimization. + * java.lang.Character being another obvious example. + * + * I know it seems like I'm being really picky here but for our application I'd + * save 1G of memory right off the bat. We'd go down from 1.152G of memory used + * down to 144M of memory used which is much better IMO. + * + * http://java.sun.com/docs/books/tutorial/native1.1/integrating/types.html + * + * @author Kevin A. Burton + * @author Greg Whalin + */ +public class NativeHandler { + + // logger + private static Logger log = + Logger.getLogger( NativeHandler.class.getName() ); + + /** + * Detemine of object can be natively serialized by this class. + * + * @param value Object to test. + * @return true/false + */ + public static boolean isHandled( Object value ) { + + return ( + value instanceof Byte || + value instanceof Boolean || + value instanceof Integer || + value instanceof Long || + value instanceof Character || + value instanceof String || + value instanceof StringBuffer || + value instanceof Float || + value instanceof Short || + value instanceof Double || + value instanceof Date || + value instanceof StringBuilder || + value instanceof byte[] + ) + ? true + : false; + } + + /** + * Returns the flag for marking the type of the byte array. + * + * @param value Object we are storing. + * @return int marker + */ + public static int getMarkerFlag( Object value ) { + + if ( value instanceof Byte ) + return MemcachedClient.MARKER_BYTE; + + if ( value instanceof Boolean ) + return MemcachedClient.MARKER_BOOLEAN; + + if ( value instanceof Integer ) + return MemcachedClient.MARKER_INTEGER; + + if ( value instanceof Long ) + return MemcachedClient.MARKER_LONG; + + if ( value instanceof Character ) + return MemcachedClient.MARKER_CHARACTER; + + if ( value instanceof String ) + return MemcachedClient.MARKER_STRING; + + if ( value instanceof StringBuffer ) + return MemcachedClient.MARKER_STRINGBUFFER; + + if ( value instanceof Float ) + return MemcachedClient.MARKER_FLOAT; + + if ( value instanceof Short ) + return MemcachedClient.MARKER_SHORT; + + if ( value instanceof Double ) + return MemcachedClient.MARKER_DOUBLE; + + if ( value instanceof Date ) + return MemcachedClient.MARKER_DATE; + + if ( value instanceof StringBuilder ) + return MemcachedClient.MARKER_STRINGBUILDER; + + if ( value instanceof byte[] ) + return MemcachedClient.MARKER_BYTEARR; + + return -1; + } + + /** + * Encodes supported types + * + * @param value Object to encode. + * @return byte array + * + * @throws Exception If fail to encode. + */ + public static byte[] encode( Object value ) throws Exception { + + if ( value instanceof Byte ) + return encode( (Byte)value ); + + if ( value instanceof Boolean ) + return encode( (Boolean)value ); + + if ( value instanceof Integer ) + return encode( ((Integer)value).intValue() ); + + if ( value instanceof Long ) + return encode( ((Long)value).longValue() ); + + if ( value instanceof Character ) + return encode( (Character)value ); + + if ( value instanceof String ) + return encode( (String)value ); + + if ( value instanceof StringBuffer ) + return encode( (StringBuffer)value ); + + if ( value instanceof Float ) + return encode( ((Float)value).floatValue() ); + + if ( value instanceof Short ) + return encode( (Short)value ); + + if ( value instanceof Double ) + return encode( ((Double)value).doubleValue() ); + + if ( value instanceof Date ) + return encode( (Date)value); + + if ( value instanceof StringBuilder ) + return encode( (StringBuilder)value ); + + if ( value instanceof byte[] ) + return encode( (byte[])value ); + + return null; + } + + protected static byte[] encode( Byte value ) { + byte[] b = new byte[1]; + b[0] = value.byteValue(); + return b; + } + + protected static byte[] encode( Boolean value ) { + byte[] b = new byte[1]; + + if ( value.booleanValue() ) + b[0] = 1; + else + b[0] = 0; + + return b; + } + + protected static byte[] encode( int value ) { + return getBytes( value ); + } + + protected static byte[] encode( long value ) throws Exception { + return getBytes( value ); + } + + protected static byte[] encode( Date value ) { + return getBytes( value.getTime() ); + } + + protected static byte[] encode( Character value ) { + return encode( value.charValue() ); + } + + protected static byte[] encode( String value ) throws Exception { + return value.getBytes( "UTF-8" ); + } + + protected static byte[] encode( StringBuffer value ) throws Exception { + return encode( value.toString() ); + } + + protected static byte[] encode( float value ) throws Exception { + return encode( (int)Float.floatToIntBits( value ) ); + } + + protected static byte[] encode( Short value ) throws Exception { + return encode( (int)value.shortValue() ); + } + + protected static byte[] encode( double value ) throws Exception { + return encode( (long)Double.doubleToLongBits( value ) ); + } + + protected static byte[] encode( StringBuilder value ) throws Exception { + return encode( value.toString() ); + } + + protected static byte[] encode( byte[] value ) { + return value; + } + + protected static byte[] getBytes( long value ) { + byte[] b = new byte[8]; + b[0] = (byte)((value >> 56) & 0xFF); + b[1] = (byte)((value >> 48) & 0xFF); + b[2] = (byte)((value >> 40) & 0xFF); + b[3] = (byte)((value >> 32) & 0xFF); + b[4] = (byte)((value >> 24) & 0xFF); + b[5] = (byte)((value >> 16) & 0xFF); + b[6] = (byte)((value >> 8) & 0xFF); + b[7] = (byte)((value >> 0) & 0xFF); + return b; + } + + protected static byte[] getBytes( int value ) { + byte[] b = new byte[4]; + b[0] = (byte)((value >> 24) & 0xFF); + b[1] = (byte)((value >> 16) & 0xFF); + b[2] = (byte)((value >> 8) & 0xFF); + b[3] = (byte)((value >> 0) & 0xFF); + return b; + } + + /** + * Decodes byte array using memcache flag to determine type. + * + * @param b + * @param marker + * @return + * @throws Exception + */ + public static Object decode( byte[] b, int flag ) throws Exception { + + if ( b.length < 1 ) + return null; + + + if ( ( flag & MemcachedClient.MARKER_BYTE ) == MemcachedClient.MARKER_BYTE ) + return decodeByte( b ); + + if ( ( flag & MemcachedClient.MARKER_BOOLEAN ) == MemcachedClient.MARKER_BOOLEAN ) + return decodeBoolean( b ); + + if ( ( flag & MemcachedClient.MARKER_INTEGER ) == MemcachedClient.MARKER_INTEGER ) + return decodeInteger( b ); + + if ( ( flag & MemcachedClient.MARKER_LONG ) == MemcachedClient.MARKER_LONG ) + return decodeLong( b ); + + if ( ( flag & MemcachedClient.MARKER_CHARACTER ) == MemcachedClient.MARKER_CHARACTER ) + return decodeCharacter( b ); + + if ( ( flag & MemcachedClient.MARKER_STRING ) == MemcachedClient.MARKER_STRING ) + return decodeString( b ); + + if ( ( flag & MemcachedClient.MARKER_STRINGBUFFER ) == MemcachedClient.MARKER_STRINGBUFFER ) + return decodeStringBuffer( b ); + + if ( ( flag & MemcachedClient.MARKER_FLOAT ) == MemcachedClient.MARKER_FLOAT ) + return decodeFloat( b ); + + if ( ( flag & MemcachedClient.MARKER_SHORT ) == MemcachedClient.MARKER_SHORT ) + return decodeShort( b ); + + if ( ( flag & MemcachedClient.MARKER_DOUBLE ) == MemcachedClient.MARKER_DOUBLE ) + return decodeDouble( b ); + + if ( ( flag & MemcachedClient.MARKER_DATE ) == MemcachedClient.MARKER_DATE ) + return decodeDate( b ); + + if ( ( flag & MemcachedClient.MARKER_STRINGBUILDER ) == MemcachedClient.MARKER_STRINGBUILDER ) + return decodeStringBuilder( b ); + + if ( ( flag & MemcachedClient.MARKER_BYTEARR ) == MemcachedClient.MARKER_BYTEARR ) + return decodeByteArr( b ); + + return null; + } + + // decode methods + protected static Byte decodeByte( byte[] b ) { + return new Byte( b[0] ); + } + + protected static Boolean decodeBoolean( byte[] b ) { + boolean value = b[0] == 1; + return ( value ) ? Boolean.TRUE : Boolean.FALSE; + } + + protected static Integer decodeInteger( byte[] b ) { + return new Integer( toInt( b ) ); + } + + protected static Long decodeLong( byte[] b ) throws Exception { + return new Long( toLong( b ) ); + } + + protected static Character decodeCharacter( byte[] b ) { + return new Character( (char)decodeInteger( b ).intValue() ); + } + + protected static String decodeString( byte[] b ) throws Exception { + return new String( b, "UTF-8" ); + } + + protected static StringBuffer decodeStringBuffer( byte[] b ) throws Exception { + return new StringBuffer( decodeString( b ) ); + } + + protected static Float decodeFloat( byte[] b ) throws Exception { + Integer l = decodeInteger( b ); + return new Float( Float.intBitsToFloat( l.intValue() ) ); + } + + protected static Short decodeShort( byte[] b ) throws Exception { + return new Short( (short)decodeInteger( b ).intValue() ); + } + + protected static Double decodeDouble( byte[] b ) throws Exception { + Long l = decodeLong( b ); + return new Double( Double.longBitsToDouble( l.longValue() ) ); + } + + protected static Date decodeDate( byte[] b ) { + return new Date( toLong( b ) ); + } + + protected static StringBuilder decodeStringBuilder( byte[] b ) throws Exception { + return new StringBuilder( decodeString( b ) ); + } + + protected static byte[] decodeByteArr( byte[] b ) { + return b; + } + + /** + * This works by taking each of the bit patterns and converting them to + * ints taking into account 2s complement and then adding them.. + * + * @param b + * @return + */ + protected static int toInt( byte[] b ) { + return (((((int) b[3]) & 0xFF) << 32) + + ((((int) b[2]) & 0xFF) << 40) + + ((((int) b[1]) & 0xFF) << 48) + + ((((int) b[0]) & 0xFF) << 56)); + } + + protected static long toLong( byte[] b ) { + return ((((long) b[7]) & 0xFF) + + ((((long) b[6]) & 0xFF) << 8) + + ((((long) b[5]) & 0xFF) << 16) + + ((((long) b[4]) & 0xFF) << 24) + + ((((long) b[3]) & 0xFF) << 32) + + ((((long) b[2]) & 0xFF) << 40) + + ((((long) b[1]) & 0xFF) << 48) + + ((((long) b[0]) & 0xFF) << 56)); + } +} diff --git a/src/main/java/com/meetup/memcached/NestedIOException.java b/src/main/java/com/meetup/memcached/NestedIOException.java new file mode 100644 index 0000000..264605e --- /dev/null +++ b/src/main/java/com/meetup/memcached/NestedIOException.java @@ -0,0 +1,44 @@ +/** + * 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 Kevin A. Burton + */ +package com.meetup.memcached; + +import java.io.*; + +/** + * Bridge class to provide nested Exceptions with IOException which has + * constructors that don't take Throwables. + * + * @author Kevin Burton + * @version 1.2 + */ +public class NestedIOException extends IOException { + + /** + * Create a new NestedIOException instance. + * @param cause object of type throwable + */ + public NestedIOException( Throwable cause ) { + super( cause.getMessage() ); + super.initCause( cause ); + } + + public NestedIOException( String message, Throwable cause ) { + super( message ); + initCause( cause ); + } +} diff --git a/src/main/java/com/meetup/memcached/SockIOPool.java b/src/main/java/com/meetup/memcached/SockIOPool.java new file mode 100644 index 0000000..c649b9a --- /dev/null +++ b/src/main/java/com/meetup/memcached/SockIOPool.java @@ -0,0 +1,1903 @@ +/** + * 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 + */ +package com.meetup.memcached; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +// java.util +import java.util.Map; +import java.util.List; +import java.util.Set; +import java.util.Iterator; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Date; +import java.util.Arrays; +import java.util.SortedMap; +import java.util.TreeMap; + +import java.util.zip.*; +import java.net.*; +import java.io.*; +import java.nio.*; +import java.nio.channels.*; +import java.util.concurrent.locks.ReentrantLock; +import org.apache.log4j.Logger; + +/** + * This class is a connection pool for maintaning a pool of persistent connections
+ * to memcached servers. + * + * The pool must be initialized prior to use. This should typically be early on
+ * in the lifecycle of the JVM instance.
+ *
+ *

An example of initializing using defaults:

+ *
+ *
+ *	static {
+ *		String[] serverlist = { "cache0.server.com:12345", "cache1.server.com:12345" };
+ *
+ *		SockIOPool pool = SockIOPool.getInstance();
+ *		pool.setServers(serverlist);
+ *		pool.initialize();	
+ *	}
+ * 
+ *

An example of initializing using defaults and providing weights for servers:

+ *
+ *	static {
+ *		String[] serverlist = { "cache0.server.com:12345", "cache1.server.com:12345" };
+ *		Integer[] weights   = { new Integer(5), new Integer(2) };
+ *		
+ *		SockIOPool pool = SockIOPool.getInstance();
+ *		pool.setServers(serverlist);
+ *		pool.setWeights(weights);	
+ *		pool.initialize();	
+ *	}
+ *  
+ *

An example of initializing overriding defaults:

+ *
+ *	static {
+ *		String[] serverlist     = { "cache0.server.com:12345", "cache1.server.com:12345" };
+ *		Integer[] weights       = { new Integer(5), new Integer(2) };	
+ *		int initialConnections  = 10;
+ *		int minSpareConnections = 5;
+ *		int maxSpareConnections = 50;	
+ *		long maxIdleTime        = 1000 * 60 * 30;	// 30 minutes
+ *		long maxBusyTime        = 1000 * 60 * 5;	// 5 minutes
+ *		long maintThreadSleep   = 1000 * 5;			// 5 seconds
+ *		int	socketTimeOut       = 1000 * 3;			// 3 seconds to block on reads
+ *		int	socketConnectTO     = 1000 * 3;			// 3 seconds to block on initial connections.  If 0, then will use blocking connect (default)
+ *		boolean failover        = false;			// turn off auto-failover in event of server down	
+ *		boolean nagleAlg        = false;			// turn off Nagle's algorithm on all sockets in pool	
+ *		boolean aliveCheck      = false;			// disable health check of socket on checkout
+ *
+ *		SockIOPool pool = SockIOPool.getInstance();
+ *		pool.setServers( serverlist );
+ *		pool.setWeights( weights );	
+ *		pool.setInitConn( initialConnections );
+ *		pool.setMinConn( minSpareConnections );
+ *		pool.setMaxConn( maxSpareConnections );
+ *		pool.setMaxIdle( maxIdleTime );
+ *		pool.setMaxBusyTime( maxBusyTime );
+ *		pool.setMaintSleep( maintThreadSleep );
+ *		pool.setSocketTO( socketTimeOut );
+ *		pool.setNagle( nagleAlg );	
+ *		pool.setHashingAlg( SockIOPool.NEW_COMPAT_HASH );
+ *		pool.setAliveCheck( true );
+ *		pool.initialize();	
+ *	}
+ *  
+ * The easiest manner in which to initialize the pool is to set the servers and rely on defaults as in the first example.
+ * After pool is initialized, a client will request a SockIO object by calling getSock with the cache key
+ * The client must always close the SockIO object when finished, which will return the connection back to the pool.
+ *

An example of retrieving a SockIO object:

+ *
+ *		SockIOPool.SockIO sock = SockIOPool.getInstance().getSock( key );
+ *		try {
+ *			sock.write( "version\r\n" );	
+ *			sock.flush();	
+ *			System.out.println( "Version: " + sock.readLine() );	
+ *		}
+ *		catch (IOException ioe) { System.out.println( "io exception thrown" ) };	
+ *
+ *		sock.close();	
+ * 
+ * + * @author greg whalin + * @version 1.5 + */ +public class SockIOPool { + + // logger + private static Logger log = + Logger.getLogger( SockIOPool.class.getName() ); + + // store instances of pools + private static Map pools = + new HashMap(); + + // avoid recurring construction + private static ThreadLocal MD5 = new ThreadLocal() { + @Override + protected MessageDigest initialValue() { + try { + return MessageDigest.getInstance( "MD5" ); + } + catch ( NoSuchAlgorithmException e ) { + log.error( "++++ no md5 algorithm found" ); + throw new IllegalStateException( "++++ no md5 algorythm found"); + } + } + }; + + // Constants + private static final Integer ZERO = new Integer( 0 ); + public static final int NATIVE_HASH = 0; // native String.hashCode(); + public static final int OLD_COMPAT_HASH = 1; // original compatibility hashing algorithm (works with other clients) + public static final int NEW_COMPAT_HASH = 2; // new CRC32 based compatibility hashing algorithm (works with other clients) + public static final int CONSISTENT_HASH = 3; // MD5 Based -- Stops thrashing when a server added or removed + + public static final long MAX_RETRY_DELAY = 10 * 60 * 1000; // max of 10 minute delay for fall off + + // Pool data + private MaintThread maintThread; + private boolean initialized = false; + private int maxCreate = 1; // this will be initialized by pool when the pool is initialized + + // initial, min and max pool sizes + private int poolMultiplier = 3; + private int initConn = 10; + private int minConn = 5; + private int maxConn = 100; + private long maxIdle = 1000 * 60 * 5; // max idle time for avail sockets + private long maxBusyTime = 1000 * 30; // max idle time for avail sockets + private long maintSleep = 1000 * 30; // maintenance thread sleep time + private int socketTO = 1000 * 3; // default timeout of socket reads + private int socketConnectTO = 1000 * 3; // default timeout of socket connections + private boolean aliveCheck = false; // default to not check each connection for being alive + private boolean failover = true; // default to failover in event of cache server dead + private boolean failback = true; // only used if failover is also set ... controls putting a dead server back into rotation + private boolean nagle = false; // enable/disable Nagle's algorithm + private int hashingAlg = NATIVE_HASH; // default to using the native hash as it is the fastest + + // locks + private final ReentrantLock hostDeadLock = new ReentrantLock(); + + // list of all servers + private String[] servers; + private Integer[] weights; + private Integer totalWeight = 0; + + private List buckets; + private TreeMap consistentBuckets; + + // dead server map + private Map hostDead; + private Map hostDeadDur; + + // map to hold all available sockets + // map to hold busy sockets + // set to hold sockets to close + private Map> availPool; + private Map> busyPool; + private Map deadPool;; + + // empty constructor + protected SockIOPool() { } + + /** + * Factory to create/retrieve new pools given a unique poolName. + * + * @param poolName unique name of the pool + * @return instance of SockIOPool + */ + public static synchronized SockIOPool getInstance( String poolName ) { + if ( pools.containsKey( poolName ) ) + return pools.get( poolName ); + + SockIOPool pool = new SockIOPool(); + pools.put( poolName, pool ); + + return pool; + } + + /** + * Single argument version of factory used for back compat. + * Simply creates a pool named "default". + * + * @return instance of SockIOPool + */ + public static SockIOPool getInstance() { + return getInstance( "default" ); + } + + /** + * Sets the list of all cache servers. + * + * @param servers String array of servers [host:port] + */ + public void setServers( String[] servers ) { this.servers = servers; } + + /** + * Returns the current list of all cache servers. + * + * @return String array of servers [host:port] + */ + public String[] getServers() { return this.servers; } + + /** + * Sets the list of weights to apply to the server list. + * + * This is an int array with each element corresponding to an element
+ * in the same position in the server String array. + * + * @param weights Integer array of weights + */ + public void setWeights( Integer[] weights ) { this.weights = weights; } + + /** + * Returns the current list of weights. + * + * @return int array of weights + */ + public Integer[] getWeights() { return this.weights; } + + /** + * Sets the initial number of connections per server in the available pool. + * + * @param initConn int number of connections + */ + public void setInitConn( int initConn ) { this.initConn = initConn; } + + /** + * Returns the current setting for the initial number of connections per server in + * the available pool. + * + * @return number of connections + */ + public int getInitConn() { return this.initConn; } + + /** + * Sets the minimum number of spare connections to maintain in our available pool. + * + * @param minConn number of connections + */ + public void setMinConn( int minConn ) { this.minConn = minConn; } + + /** + * Returns the minimum number of spare connections in available pool. + * + * @return number of connections + */ + public int getMinConn() { return this.minConn; } + + /** + * Sets the maximum number of spare connections allowed in our available pool. + * + * @param maxConn number of connections + */ + public void setMaxConn( int maxConn ) { this.maxConn = maxConn; } + + /** + * Returns the maximum number of spare connections allowed in available pool. + * + * @return number of connections + */ + public int getMaxConn() { return this.maxConn; } + + /** + * Sets the max idle time for threads in the available pool. + * + * @param maxIdle idle time in ms + */ + public void setMaxIdle( long maxIdle ) { this.maxIdle = maxIdle; } + + /** + * Returns the current max idle setting. + * + * @return max idle setting in ms + */ + public long getMaxIdle() { return this.maxIdle; } + + /** + * Sets the max busy time for threads in the busy pool. + * + * @param maxBusyTime idle time in ms + */ + public void setMaxBusyTime( long maxBusyTime ) { this.maxBusyTime = maxBusyTime; } + + /** + * Returns the current max busy setting. + * + * @return max busy setting in ms + */ + public long getMaxBusy() { return this.maxBusyTime; } + + /** + * Set the sleep time between runs of the pool maintenance thread. + * If set to 0, then the maint thread will not be started. + * + * @param maintSleep sleep time in ms + */ + public void setMaintSleep( long maintSleep ) { this.maintSleep = maintSleep; } + + /** + * Returns the current maint thread sleep time. + * + * @return sleep time in ms + */ + public long getMaintSleep() { return this.maintSleep; } + + /** + * Sets the socket timeout for reads. + * + * @param socketTO timeout in ms + */ + public void setSocketTO( int socketTO ) { this.socketTO = socketTO; } + + /** + * Returns the socket timeout for reads. + * + * @return timeout in ms + */ + public int getSocketTO() { return this.socketTO; } + + /** + * Sets the socket timeout for connect. + * + * @param socketConnectTO timeout in ms + */ + public void setSocketConnectTO( int socketConnectTO ) { this.socketConnectTO = socketConnectTO; } + + /** + * Returns the socket timeout for connect. + * + * @return timeout in ms + */ + public int getSocketConnectTO() { return this.socketConnectTO; } + + /** + * Sets the failover flag for the pool. + * + * If this flag is set to true, and a socket fails to connect,
+ * the pool will attempt to return a socket from another server
+ * if one exists. If set to false, then getting a socket
+ * will return null if it fails to connect to the requested server. + * + * @param failover true/false + */ + public void setFailover( boolean failover ) { this.failover = failover; } + + /** + * Returns current state of failover flag. + * + * @return true/false + */ + public boolean getFailover() { return this.failover; } + + /** + * Sets the failback flag for the pool. + * + * If this is true and we have marked a host as dead, + * will try to bring it back. If it is false, we will never + * try to resurrect a dead host. + * + * @param failback true/false + */ + public void setFailback( boolean failback ) { this.failback = failback; } + + /** + * Returns current state of failover flag. + * + * @return true/false + */ + public boolean getFailback() { return this.failback; } + + /** + * Sets the aliveCheck flag for the pool. + * + * When true, this will attempt to talk to the server on + * every connection checkout to make sure the connection is + * still valid. This adds extra network chatter and thus is + * defaulted off. May be useful if you want to ensure you do + * not have any problems talking to the server on a dead connection. + * + * @param aliveCheck true/false + */ + public void setAliveCheck( boolean aliveCheck ) { this.aliveCheck = aliveCheck; } + + + /** + * Returns the current status of the aliveCheck flag. + * + * @return true / false + */ + public boolean getAliveCheck() { return this.aliveCheck; } + + /** + * Sets the Nagle alg flag for the pool. + * + * If false, will turn off Nagle's algorithm on all sockets created. + * + * @param nagle true/false + */ + public void setNagle( boolean nagle ) { this.nagle = nagle; } + + /** + * Returns current status of nagle flag + * + * @return true/false + */ + public boolean getNagle() { return this.nagle; } + + /** + * Sets the hashing algorithm we will use. + * + * The types are as follows. + * + * SockIOPool.NATIVE_HASH (0) - native String.hashCode() - fast (cached) but not compatible with other clients + * SockIOPool.OLD_COMPAT_HASH (1) - original compatibility hashing alg (works with other clients) + * SockIOPool.NEW_COMPAT_HASH (2) - new CRC32 based compatibility hashing algorithm (fast and works with other clients) + * + * @param alg int value representing hashing algorithm + */ + public void setHashingAlg( int alg ) { this.hashingAlg = alg; } + + /** + * Returns current status of customHash flag + * + * @return true/false + */ + public int getHashingAlg() { return this.hashingAlg; } + + /** + * Internal private hashing method. + * + * This is the original hashing algorithm from other clients. + * Found to be slow and have poor distribution. + * + * @param key String to hash + * @return hashCode for this string using our own hashing algorithm + */ + private static long origCompatHashingAlg( String key ) { + long hash = 0; + char[] cArr = key.toCharArray(); + + for ( int i = 0; i < cArr.length; ++i ) { + hash = (hash * 33) + cArr[i]; + } + + return hash; + } + + /** + * Internal private hashing method. + * + * This is the new hashing algorithm from other clients. + * Found to be fast and have very good distribution. + * + * UPDATE: This is dog slow under java + * + * @param key + * @return + */ + private static long newCompatHashingAlg( String key ) { + CRC32 checksum = new CRC32(); + checksum.update( key.getBytes() ); + long crc = checksum.getValue(); + return (crc >> 16) & 0x7fff; + } + + /** + * Internal private hashing method. + * + * MD5 based hash algorithm for use in the consistent + * hashing approach. + * + * @param key + * @return + */ + private static long md5HashingAlg( String key ) { + MessageDigest md5 = MD5.get(); + md5.reset(); + md5.update( key.getBytes() ); + byte[] bKey = md5.digest(); + long res = ((long)(bKey[3]&0xFF) << 24) | ((long)(bKey[2]&0xFF) << 16) | ((long)(bKey[1]&0xFF) << 8) | (long)(bKey[0]&0xFF); + return res; + } + + /** + * Returns a bucket to check for a given key. + * + * @param key String key cache is stored under + * @return int bucket + */ + private long getHash( String key, Integer hashCode ) { + + if ( hashCode != null ) { + if ( hashingAlg == CONSISTENT_HASH ) + return hashCode.longValue() & 0xffffffffL; + else + return hashCode.longValue(); + } + else { + switch ( hashingAlg ) { + case NATIVE_HASH: + return (long)key.hashCode(); + case OLD_COMPAT_HASH: + return origCompatHashingAlg( key ); + case NEW_COMPAT_HASH: + return newCompatHashingAlg( key ); + case CONSISTENT_HASH: + return md5HashingAlg( key ); + default: + // use the native hash as a default + hashingAlg = NATIVE_HASH; + return (long)key.hashCode(); + } + } + } + + private long getBucket( String key, Integer hashCode ) { + long hc = getHash( key, hashCode ); + + if ( this.hashingAlg == CONSISTENT_HASH ) { + return findPointFor( hc ); + } + else { + long bucket = hc % buckets.size(); + if ( bucket < 0 ) bucket *= -1; + return bucket; + } + } + + /** + * Gets the first available key equal or above the given one, if none found, + * returns the first k in the bucket + * @param k key + * @return + */ + private Long findPointFor( Long hv ) { + // this works in java 6, but still want to release support for java5 + //Long k = this.consistentBuckets.ceilingKey( hv ); + //return ( k == null ) ? this.consistentBuckets.firstKey() : k; + + SortedMap tmap = + this.consistentBuckets.tailMap( hv ); + + return ( tmap.isEmpty() ) ? this.consistentBuckets.firstKey() : tmap.firstKey(); + } + + /** + * Initializes the pool. + */ + public void initialize() { + + synchronized( this ) { + + // check to see if already initialized + if ( initialized + && ( buckets != null || consistentBuckets != null ) + && ( availPool != null ) + && ( busyPool != null ) ) { + log.error( "++++ trying to initialize an already initialized pool" ); + return; + } + + // pools + availPool = new HashMap>( servers.length * initConn ); + busyPool = new HashMap>( servers.length * initConn ); + deadPool = new IdentityHashMap(); + + hostDeadDur = new HashMap(); + hostDead = new HashMap(); + maxCreate = (poolMultiplier > minConn) ? minConn : minConn / poolMultiplier; // only create up to maxCreate connections at once + + if ( log.isDebugEnabled() ) { + log.debug( "++++ initializing pool with following settings:" ); + log.debug( "++++ initial size: " + initConn ); + log.debug( "++++ min spare : " + minConn ); + log.debug( "++++ max spare : " + maxConn ); + } + + // if servers is not set, or it empty, then + // throw a runtime exception + if ( servers == null || servers.length <= 0 ) { + log.error( "++++ trying to initialize with no servers" ); + throw new IllegalStateException( "++++ trying to initialize with no servers" ); + } + + // initalize our internal hashing structures + if ( this.hashingAlg == CONSISTENT_HASH ) + populateConsistentBuckets(); + else + populateBuckets(); + + // mark pool as initialized + this.initialized = true; + + // start maint thread + if ( this.maintSleep > 0 ) + this.startMaintThread(); + } + } + + private void populateBuckets() { + if ( log.isDebugEnabled() ) + log.debug( "++++ initializing internal hashing structure for consistent hashing" ); + + // store buckets in tree map + this.buckets = new ArrayList(); + + for ( int i = 0; i < servers.length; i++ ) { + if ( this.weights != null && this.weights.length > i ) { + for ( int k = 0; k < this.weights[i].intValue(); k++ ) { + this.buckets.add( servers[i] ); + if ( log.isDebugEnabled() ) + log.debug( "++++ added " + servers[i] + " to server bucket" ); + } + } + else { + this.buckets.add( servers[i] ); + if ( log.isDebugEnabled() ) + log.debug( "++++ added " + servers[i] + " to server bucket" ); + } + + // create initial connections + if ( log.isDebugEnabled() ) + log.debug( "+++ creating initial connections (" + initConn + ") for host: " + servers[i] ); + + for ( int j = 0; j < initConn; j++ ) { + SockIO socket = createSocket( servers[i] ); + if ( socket == null ) { + log.error( "++++ failed to create connection to: " + servers[i] + " -- only " + j + " created." ); + break; + } + + addSocketToPool( availPool, servers[i], socket ); + if ( log.isDebugEnabled() ) + log.debug( "++++ created and added socket: " + socket.toString() + " for host " + servers[i] ); + } + } + } + + private void populateConsistentBuckets() { + if ( log.isDebugEnabled() ) + log.debug( "++++ initializing internal hashing structure for consistent hashing" ); + + // store buckets in tree map + this.consistentBuckets = new TreeMap(); + + MessageDigest md5 = MD5.get(); + if ( this.totalWeight <= 0 && this.weights != null ) { + for ( int i = 0; i < this.weights.length; i++ ) + this.totalWeight += ( this.weights[i] == null ) ? 1 : this.weights[i]; + } + else if ( this.weights == null ) { + this.totalWeight = this.servers.length; + } + + for ( int i = 0; i < servers.length; i++ ) { + int thisWeight = 1; + if ( this.weights != null && this.weights[i] != null ) + thisWeight = this.weights[i]; + + double factor = Math.floor( ((double)(40 * this.servers.length * thisWeight)) / (double)this.totalWeight ); + + for ( long j = 0; j < factor; j++ ) { + byte[] d = md5.digest( ( servers[i] + "-" + j ).getBytes() ); + for ( int h = 0 ; h < 4; h++ ) { + Long k = + ((long)(d[3+h*4]&0xFF) << 24) + | ((long)(d[2+h*4]&0xFF) << 16) + | ((long)(d[1+h*4]&0xFF) << 8) + | ((long)(d[0+h*4]&0xFF)); + + consistentBuckets.put( k, servers[i] ); + if ( log.isDebugEnabled() ) + log.debug( "++++ added " + servers[i] + " to server bucket" ); + } + } + + // create initial connections + if ( log.isDebugEnabled() ) + log.debug( "+++ creating initial connections (" + initConn + ") for host: " + servers[i] ); + + for ( int j = 0; j < initConn; j++ ) { + SockIO socket = createSocket( servers[i] ); + if ( socket == null ) { + log.error( "++++ failed to create connection to: " + servers[i] + " -- only " + j + " created." ); + break; + } + + addSocketToPool( availPool, servers[i], socket ); + if ( log.isDebugEnabled() ) + log.debug( "++++ created and added socket: " + socket.toString() + " for host " + servers[i] ); + } + } + } + + /** + * Returns state of pool. + * + * @return true if initialized. + */ + public boolean isInitialized() { + return initialized; + } + + /** + * Creates a new SockIO obj for the given server. + * + * If server fails to connect, then return null and do not try
+ * again until a duration has passed. This duration will grow
+ * by doubling after each failed attempt to connect. + * + * @param host host:port to connect to + * @return SockIO obj or null if failed to create + */ + protected SockIO createSocket( String host ) { + + SockIO socket = null; + + // if host is dead, then we don't need to try again + // until the dead status has expired + // we do not try to put back in if failback is off + hostDeadLock.lock(); + try { + if ( failover && failback && hostDead.containsKey( host ) && hostDeadDur.containsKey( host ) ) { + + Date store = hostDead.get( host ); + long expire = hostDeadDur.get( host ).longValue(); + + if ( (store.getTime() + expire) > System.currentTimeMillis() ) + return null; + } + } + finally { + hostDeadLock.unlock(); + } + + try { + socket = new SockIO( this, host, this.socketTO, this.socketConnectTO, this.nagle ); + + if ( !socket.isConnected() ) { + log.error( "++++ failed to get SockIO obj for: " + host + " -- new socket is not connected" ); + deadPool.put( socket, ZERO ); + socket = null; + } + } + catch ( Exception ex ) { + log.error( "++++ failed to get SockIO obj for: " + host ); + log.error( ex.getMessage(), ex ); + socket = null; + } + + // if we failed to get socket, then mark + // host dead for a duration which falls off + hostDeadLock.lock(); + try { + if ( socket == null ) { + Date now = new Date(); + hostDead.put( host, now ); + + long expire = ( hostDeadDur.containsKey( host ) ) ? (((Long)hostDeadDur.get( host )).longValue() * 2) : 1000; + + if ( expire > MAX_RETRY_DELAY ) + expire = MAX_RETRY_DELAY; + + hostDeadDur.put( host, new Long( expire ) ); + if ( log.isDebugEnabled() ) + log.debug( "++++ ignoring dead host: " + host + " for " + expire + " ms" ); + + // also clear all entries for this host from availPool + clearHostFromPool( availPool, host ); + } + else { + if ( log.isDebugEnabled() ) + log.debug( "++++ created socket (" + socket.toString() + ") for host: " + host ); + if ( hostDead.containsKey( host ) || hostDeadDur.containsKey( host ) ) { + hostDead.remove( host ); + hostDeadDur.remove( host ); + } + } + } + finally { + hostDeadLock.unlock(); + } + + return socket; + } + + /** + * @param key + * @return + */ + public String getHost( String key ) { + return getHost( key, null ); + } + + /** + * Gets the host that a particular key / hashcode resides on. + * + * @param key + * @param hashcode + * @return + */ + public String getHost( String key, Integer hashcode ) { + SockIO socket = getSock( key, hashcode ); + String host = socket.getHost(); + socket.close(); + return host; + } + + /** + * Returns appropriate SockIO object given + * string cache key. + * + * @param key hashcode for cache key + * @return SockIO obj connected to server + */ + public SockIO getSock( String key ) { + return getSock( key, null ); + } + + /** + * Returns appropriate SockIO object given + * string cache key and optional hashcode. + * + * Trys to get SockIO from pool. Fails over + * to additional pools in event of server failure. + * + * @param key hashcode for cache key + * @param hashCode if not null, then the int hashcode to use + * @return SockIO obj connected to server + */ + public SockIO getSock( String key, Integer hashCode ) { + + if ( log.isDebugEnabled() ) + log.debug( "cache socket pick " + key + " " + hashCode ); + + if ( !this.initialized ) { + log.error( "attempting to get SockIO from uninitialized pool!" ); + return null; + } + + // if no servers return null + if ( ( this.hashingAlg == CONSISTENT_HASH && consistentBuckets.size() == 0 ) + || ( buckets != null && buckets.size() == 0 ) ) + return null; + + // if only one server, return it + if ( ( this.hashingAlg == CONSISTENT_HASH && consistentBuckets.size() == 1 ) + || ( buckets != null && buckets.size() == 1 ) ) { + + SockIO sock = ( this.hashingAlg == CONSISTENT_HASH ) + ? getConnection( consistentBuckets.get( consistentBuckets.firstKey() ) ) + : getConnection( buckets.get( 0 ) ); + + if ( sock != null && sock.isConnected() ) { + if ( aliveCheck ) { + if ( !sock.isAlive() ) { + sock.close(); + try { sock.trueClose(); } catch ( IOException ioe ) { log.error( "failed to close dead socket" ); } + sock = null; + } + } + } + else { + if ( sock != null ) { + deadPool.put( sock, ZERO ); + sock = null; + } + } + + return sock; + } + + // from here on, we are working w/ multiple servers + // keep trying different servers until we find one + // making sure we only try each server one time + Set tryServers = new HashSet( Arrays.asList( servers ) ); + + // get initial bucket + long bucket = getBucket( key, hashCode ); + String server = ( this.hashingAlg == CONSISTENT_HASH ) + ? consistentBuckets.get( bucket ) + : buckets.get( (int)bucket ); + + while ( !tryServers.isEmpty() ) { + + // try to get socket from bucket + SockIO sock = getConnection( server ); + + if ( log.isDebugEnabled() ) + log.debug( "cache choose " + server + " for " + key ); + + if ( sock != null && sock.isConnected() ) { + if ( aliveCheck ) { + if ( sock.isAlive() ) { + return sock; + } + else { + sock.close(); + try { sock.trueClose(); } catch ( IOException ioe ) { log.error( "failed to close dead socket" ); } + sock = null; + } + } + else { + return sock; + } + } + else { + if ( sock != null ) { + deadPool.put( sock, ZERO ); + sock = null; + } + } + + // if we do not want to failover, then bail here + if ( !failover ) + return null; + + // log that we tried + tryServers.remove( server ); + + if ( tryServers.isEmpty() ) + break; + + // if we failed to get a socket from this server + // then we try again by adding an incrementer to the + // current key and then rehashing + int rehashTries = 0; + while ( !tryServers.contains( server ) ) { + + String newKey = String.format( "%s%s", rehashTries, key ); + if ( log.isDebugEnabled() ) + log.debug( "rehashing with: " + newKey ); + + bucket = getBucket( newKey, null ); + server = ( this.hashingAlg == CONSISTENT_HASH ) + ? consistentBuckets.get( bucket ) + : buckets.get( (int)bucket ); + + rehashTries++; + } + } + + return null; + } + + /** + * Returns a SockIO object from the pool for the passed in host. + * + * Meant to be called from a more intelligent method
+ * which handles choosing appropriate server
+ * and failover. + * + * @param host host from which to retrieve object + * @return SockIO object or null if fail to retrieve one + */ + public SockIO getConnection( String host ) { + + if ( !this.initialized ) { + log.error( "attempting to get SockIO from uninitialized pool!" ); + return null; + } + + if ( host == null ) + return null; + + synchronized( this ) { + + // if we have items in the pool + // then we can return it + if ( availPool != null && !availPool.isEmpty() ) { + + // take first connected socket + Map aSockets = availPool.get( host ); + + if ( aSockets != null && !aSockets.isEmpty() ) { + + for ( Iterator i = aSockets.keySet().iterator(); i.hasNext(); ) { + SockIO socket = i.next(); + + if ( socket.isConnected() ) { + if ( log.isDebugEnabled() ) + log.debug( "++++ moving socket for host (" + host + ") to busy pool ... socket: " + socket ); + + // remove from avail pool + i.remove(); + + // add to busy pool + addSocketToPool( busyPool, host, socket ); + + // return socket + return socket; + } + else { + // add to deadpool for later reaping + deadPool.put( socket, ZERO ); + + // remove from avail pool + i.remove(); + } + } + } + } + } + + // create one socket -- let the maint thread take care of creating more + SockIO socket = createSocket( host ); + if ( socket != null ) { + synchronized( this ) { + addSocketToPool( busyPool, host, socket ); + } + } + + return socket; + } + + /** + * Adds a socket to a given pool for the given host. + * THIS METHOD IS NOT THREADSAFE, SO BE CAREFUL WHEN USING! + * + * Internal utility method. + * + * @param pool pool to add to + * @param host host this socket is connected to + * @param socket socket to add + */ + protected void addSocketToPool( Map> pool, String host, SockIO socket ) { + + if ( pool.containsKey( host ) ) { + Map sockets = pool.get( host ); + + if ( sockets != null ) { + sockets.put( socket, new Long( System.currentTimeMillis() ) ); + return; + } + } + + Map sockets = + new IdentityHashMap(); + + sockets.put( socket, new Long( System.currentTimeMillis() ) ); + pool.put( host, sockets ); + } + + /** + * Removes a socket from specified pool for host. + * THIS METHOD IS NOT THREADSAFE, SO BE CAREFUL WHEN USING! + * + * Internal utility method. + * + * @param pool pool to remove from + * @param host host pool + * @param socket socket to remove + */ + protected void removeSocketFromPool( Map> pool, String host, SockIO socket ) { + if ( pool.containsKey( host ) ) { + Map sockets = pool.get( host ); + if ( sockets != null ) + sockets.remove( socket ); + } + } + + /** + * Closes and removes all sockets from specified pool for host. + * THIS METHOD IS NOT THREADSAFE, SO BE CAREFUL WHEN USING! + * + * Internal utility method. + * + * @param pool pool to clear + * @param host host to clear + */ + protected void clearHostFromPool( Map> pool, String host ) { + + if ( pool.containsKey( host ) ) { + Map sockets = pool.get( host ); + + if ( sockets != null && sockets.size() > 0 ) { + for ( Iterator i = sockets.keySet().iterator(); i.hasNext(); ) { + SockIO socket = i.next(); + try { + socket.trueClose(); + } + catch ( IOException ioe ) { + log.error( "++++ failed to close socket: " + ioe.getMessage() ); + } + + i.remove(); + socket = null; + } + } + } + } + + /** + * Checks a SockIO object in with the pool. + * + * This will remove SocketIO from busy pool, and optionally
+ * add to avail pool. + * + * @param socket socket to return + * @param addToAvail add to avail pool if true + */ + private void checkIn( SockIO socket, boolean addToAvail ) { + + String host = socket.getHost(); + if ( log.isDebugEnabled() ) + log.debug( "++++ calling check-in on socket: " + socket.toString() + " for host: " + host ); + + synchronized( this ) { + // remove from the busy pool + if ( log.isDebugEnabled() ) + log.debug( "++++ removing socket (" + socket.toString() + ") from busy pool for host: " + host ); + removeSocketFromPool( busyPool, host, socket ); + + if ( socket.isConnected() && addToAvail ) { + // add to avail pool + if ( log.isDebugEnabled() ) + log.debug( "++++ returning socket (" + socket.toString() + " to avail pool for host: " + host ); + addSocketToPool( availPool, host, socket ); + } + else { + deadPool.put( socket, ZERO ); + socket = null; + } + } + } + + /** + * Returns a socket to the avail pool. + * + * This is called from SockIO.close(). Calling this method
+ * directly without closing the SockIO object first
+ * will cause an IOException to be thrown. + * + * @param socket socket to return + */ + private void checkIn( SockIO socket ) { + checkIn( socket, true ); + } + + /** + * Closes all sockets in the passed in pool. + * + * Internal utility method. + * + * @param pool pool to close + */ + protected void closePool( Map> pool ) { + for ( Iterator i = pool.keySet().iterator(); i.hasNext(); ) { + String host = i.next(); + Map sockets = pool.get( host ); + + for ( Iterator j = sockets.keySet().iterator(); j.hasNext(); ) { + SockIO socket = j.next(); + + try { + socket.trueClose(); + } + catch ( IOException ioe ) { + log.error( "++++ failed to trueClose socket: " + socket.toString() + " for host: " + host ); + } + + j.remove(); + socket = null; + } + } + } + + /** + * Shuts down the pool. + * + * Cleanly closes all sockets.
+ * Stops the maint thread.
+ * Nulls out all internal maps
+ */ + public void shutDown() { + synchronized( this ) { + if ( log.isDebugEnabled() ) + log.debug( "++++ SockIOPool shutting down..." ); + + if ( maintThread != null && maintThread.isRunning() ) { + // stop the main thread + stopMaintThread(); + + // wait for the thread to finish + while ( maintThread.isRunning() ) { + if ( log.isDebugEnabled() ) + log.debug( "++++ waiting for main thread to finish run +++" ); + try { Thread.sleep( 500 ); } catch ( Exception ex ) { } + } + } + + if ( log.isDebugEnabled() ) + log.debug( "++++ closing all internal pools." ); + closePool( availPool ); + closePool( busyPool ); + availPool = null; + busyPool = null; + buckets = null; + consistentBuckets = null; + hostDeadDur = null; + hostDead = null; + maintThread = null; + initialized = false; + if ( log.isDebugEnabled() ) + log.debug( "++++ SockIOPool finished shutting down." ); + } + } + + /** + * Starts the maintenance thread. + * + * This thread will manage the size of the active pool
+ * as well as move any closed, but not checked in sockets
+ * back to the available pool. + */ + protected void startMaintThread() { + + if ( maintThread != null ) { + + if ( maintThread.isRunning() ) { + log.error( "main thread already running" ); + } + else { + maintThread.start(); + } + } + else { + maintThread = new MaintThread( this ); + maintThread.setInterval( this.maintSleep ); + maintThread.start(); + } + } + + /** + * Stops the maintenance thread. + */ + protected void stopMaintThread() { + if ( maintThread != null && maintThread.isRunning() ) + maintThread.stopThread(); + } + + /** + * Runs self maintenance on all internal pools. + * + * This is typically called by the maintenance thread to manage pool size. + */ + protected void selfMaint() { + if ( log.isDebugEnabled() ) + log.debug( "++++ Starting self maintenance...." ); + + // go through avail sockets and create sockets + // as needed to maintain pool settings + Map needSockets = + new HashMap(); + + synchronized( this ) { + // find out how many to create + for ( Iterator i = availPool.keySet().iterator(); i.hasNext(); ) { + String host = i.next(); + Map sockets = availPool.get( host ); + + if ( log.isDebugEnabled() ) + log.debug( "++++ Size of avail pool for host (" + host + ") = " + sockets.size() ); + + // if pool is too small (n < minSpare) + if ( sockets.size() < minConn ) { + // need to create new sockets + int need = minConn - sockets.size(); + needSockets.put( host, need ); + } + } + } + + // now create + Map> newSockets = + new HashMap>(); + + for ( String host : needSockets.keySet() ) { + Integer need = needSockets.get( host ); + + if ( log.isDebugEnabled() ) + log.debug( "++++ Need to create " + need + " new sockets for pool for host: " + host ); + + Set newSock = new HashSet( need ); + for ( int j = 0; j < need; j++ ) { + SockIO socket = createSocket( host ); + + if ( socket == null ) + break; + + newSock.add( socket ); + } + + newSockets.put( host, newSock ); + } + + // synchronize to add and remove to/from avail pool + // as well as clean up the busy pool (no point in releasing + // lock here as should be quick to pool adjust and no + // blocking ops here) + synchronized( this ) { + for ( String host : newSockets.keySet() ) { + Set sockets = newSockets.get( host ); + for ( SockIO socket : sockets ) + addSocketToPool( availPool, host, socket ); + } + + for ( Iterator i = availPool.keySet().iterator(); i.hasNext(); ) { + String host = i.next(); + Map sockets = availPool.get( host ); + if ( log.isDebugEnabled() ) + log.debug( "++++ Size of avail pool for host (" + host + ") = " + sockets.size() ); + + if ( sockets.size() > maxConn ) { + // need to close down some sockets + int diff = sockets.size() - maxConn; + int needToClose = (diff <= poolMultiplier) + ? diff + : (diff) / poolMultiplier; + + if ( log.isDebugEnabled() ) + log.debug( "++++ need to remove " + needToClose + " spare sockets for pool for host: " + host ); + + for ( Iterator j = sockets.keySet().iterator(); j.hasNext(); ) { + if ( needToClose <= 0 ) + break; + + // remove stale entries + SockIO socket = j.next(); + long expire = sockets.get( socket ).longValue(); + + // if past idle time + // then close socket + // and remove from pool + if ( (expire + maxIdle) < System.currentTimeMillis() ) { + if ( log.isDebugEnabled() ) + log.debug( "+++ removing stale entry from pool as it is past its idle timeout and pool is over max spare" ); + + // remove from the availPool + deadPool.put( socket, ZERO ); + j.remove(); + needToClose--; + } + } + } + } + + // go through busy sockets and destroy sockets + // as needed to maintain pool settings + for ( Iterator i = busyPool.keySet().iterator(); i.hasNext(); ) { + + String host = i.next(); + Map sockets = busyPool.get( host ); + + if ( log.isDebugEnabled() ) + log.debug( "++++ Size of busy pool for host (" + host + ") = " + sockets.size() ); + + // loop through all connections and check to see if we have any hung connections + for ( Iterator j = sockets.keySet().iterator(); j.hasNext(); ) { + // remove stale entries + SockIO socket = j.next(); + long hungTime = sockets.get( socket ).longValue(); + + // if past max busy time + // then close socket + // and remove from pool + if ( (hungTime + maxBusyTime) < System.currentTimeMillis() ) { + log.error( "+++ removing potentially hung connection from busy pool ... socket in pool for " + (System.currentTimeMillis() - hungTime) + "ms" ); + + // remove from the busy pool + deadPool.put( socket, ZERO ); + j.remove(); + } + } + } + } + + // finally clean out the deadPool + Set toClose; + synchronized( deadPool ) { + toClose = deadPool.keySet(); + deadPool = new IdentityHashMap(); + } + + for ( SockIO socket : toClose ) { + try { + socket.trueClose( false ); + } + catch ( Exception ex ) { + log.error( "++++ failed to close SockIO obj from deadPool" ); + log.error( ex.getMessage(), ex ); + } + + socket = null; + } + + if ( log.isDebugEnabled() ) + log.debug( "+++ ending self maintenance." ); + } + + /** + * Class which extends thread and handles maintenance of the pool. + * + * @author greg whalin + * @version 1.5 + */ + protected static class MaintThread extends Thread { + + // logger + private static Logger log = + Logger.getLogger( MaintThread.class.getName() ); + + private SockIOPool pool; + private long interval = 1000 * 3; // every 3 seconds + private boolean stopThread = false; + private boolean running; + + protected MaintThread( SockIOPool pool ) { + this.pool = pool; + this.setDaemon( true ); + this.setName( "MaintThread" ); + } + + public void setInterval( long interval ) { this.interval = interval; } + + public boolean isRunning() { + return this.running; + } + + /** + * sets stop variable + * and interupts any wait + */ + public void stopThread() { + this.stopThread = true; + this.interrupt(); + } + + /** + * Start the thread. + */ + public void run() { + this.running = true; + + while ( !this.stopThread ) { + try { + Thread.sleep( interval ); + + // if pool is initialized, then + // run the maintenance method on itself + if ( pool.isInitialized() ) + pool.selfMaint(); + + } + catch ( Exception e ) { + break; + } + } + + this.running = false; + } + } + + /** + * MemCached client for Java, utility class for Socket IO. + * + * This class is a wrapper around a Socket and its streams. + * + * @author greg whalin + * @author Richard 'toast' Russo + * @version 1.5 + */ + public static class SockIO implements LineInputStream { + + // logger + private static Logger log = + Logger.getLogger( SockIO.class.getName() ); + + // pool + private SockIOPool pool; + + // data + private String host; + private Socket sock; + + private DataInputStream in; + private BufferedOutputStream out; + + /** + * creates a new SockIO object wrapping a socket + * connection to host:port, and its input and output streams + * + * @param pool Pool this object is tied to + * @param host host to connect to + * @param port port to connect to + * @param timeout int ms to block on data for read + * @param connectTimeout timeout (in ms) for initial connection + * @param noDelay TCP NODELAY option? + * @throws IOException if an io error occurrs when creating socket + * @throws UnknownHostException if hostname is invalid + */ + public SockIO( SockIOPool pool, String host, int port, int timeout, int connectTimeout, boolean noDelay ) throws IOException, UnknownHostException { + + this.pool = pool; + + // get a socket channel + sock = getSocket( host, port, connectTimeout ); + + if ( timeout >= 0 ) + sock.setSoTimeout( timeout ); + + // testing only + sock.setTcpNoDelay( noDelay ); + + // wrap streams + in = new DataInputStream( new BufferedInputStream( sock.getInputStream() ) ); + out = new BufferedOutputStream( sock.getOutputStream() ); + + this.host = host + ":" + port; + } + + /** + * creates a new SockIO object wrapping a socket + * connection to host:port, and its input and output streams + * + * @param host hostname:port + * @param timeout read timeout value for connected socket + * @param connectTimeout timeout for initial connections + * @param noDelay TCP NODELAY option? + * @throws IOException if an io error occurrs when creating socket + * @throws UnknownHostException if hostname is invalid + */ + public SockIO( SockIOPool pool, String host, int timeout, int connectTimeout, boolean noDelay ) throws IOException, UnknownHostException { + + this.pool = pool; + + String[] ip = host.split(":"); + + // get socket: default is to use non-blocking connect + sock = getSocket( ip[ 0 ], Integer.parseInt( ip[ 1 ] ), connectTimeout ); + + if ( timeout >= 0 ) + this.sock.setSoTimeout( timeout ); + + // testing only + sock.setTcpNoDelay( noDelay ); + + // wrap streams + in = new DataInputStream( new BufferedInputStream( sock.getInputStream() ) ); + out = new BufferedOutputStream( sock.getOutputStream() ); + + this.host = host; + } + + /** + * Method which gets a connection from SocketChannel. + * + * @param host host to establish connection to + * @param port port on that host + * @param timeout connection timeout in ms + * + * @return connected socket + * @throws IOException if errors connecting or if connection times out + */ + protected static Socket getSocket( String host, int port, int timeout ) throws IOException { + SocketChannel sock = SocketChannel.open(); + sock.socket().connect( new InetSocketAddress( host, port ), timeout ); + return sock.socket(); + } + + /** + * Lets caller get access to underlying channel. + * + * @return the backing SocketChannel + */ + public SocketChannel getChannel() { return sock.getChannel(); } + + /** + * returns the host this socket is connected to + * + * @return String representation of host (hostname:port) + */ + public String getHost() { return this.host; } + + /** + * closes socket and all streams connected to it + * + * @throws IOException if fails to close streams or socket + */ + public void trueClose() throws IOException { + trueClose( true ); + } + + /** + * closes socket and all streams connected to it + * + * @throws IOException if fails to close streams or socket + */ + public void trueClose( boolean addToDeadPool ) throws IOException { + if ( log.isDebugEnabled() ) + log.debug( "++++ Closing socket for real: " + toString() ); + + boolean err = false; + StringBuilder errMsg = new StringBuilder(); + + if ( in != null ) { + try { + in.close(); + } + catch( IOException ioe ) { + log.error( "++++ error closing input stream for socket: " + toString() + " for host: " + getHost() ); + log.error( ioe.getMessage(), ioe ); + errMsg.append( "++++ error closing input stream for socket: " + toString() + " for host: " + getHost() + "\n" ); + errMsg.append( ioe.getMessage() ); + err = true; + } + } + + if ( out != null ) { + try { + out.close(); + } + catch ( IOException ioe ) { + log.error( "++++ error closing output stream for socket: " + toString() + " for host: " + getHost() ); + log.error( ioe.getMessage(), ioe ); + errMsg.append( "++++ error closing output stream for socket: " + toString() + " for host: " + getHost() + "\n" ); + errMsg.append( ioe.getMessage() ); + err = true; + } + } + + if ( sock != null ) { + try { + sock.close(); + } + catch ( IOException ioe ) { + log.error( "++++ error closing socket: " + toString() + " for host: " + getHost() ); + log.error( ioe.getMessage(), ioe ); + errMsg.append( "++++ error closing socket: " + toString() + " for host: " + getHost() + "\n" ); + errMsg.append( ioe.getMessage() ); + err = true; + } + } + + // check in to pool + if ( addToDeadPool && sock != null ) + pool.checkIn( this, false ); + + in = null; + out = null; + sock = null; + + if ( err ) + throw new IOException( errMsg.toString() ); + } + + /** + * sets closed flag and checks in to connection pool + * but does not close connections + */ + void close() { + // check in to pool + if ( log.isDebugEnabled() ) + log.debug("++++ marking socket (" + this.toString() + ") as closed and available to return to avail pool"); + pool.checkIn( this ); + } + + /** + * checks if the connection is open + * + * @return true if connected + */ + boolean isConnected() { + return ( sock != null && sock.isConnected() ); + } + + /* + * checks to see that the connection is still working + * + * @return true if still alive + */ + boolean isAlive() { + + if ( !isConnected() ) + return false; + + // try to talk to the server w/ a dumb query to ask its version + try { + this.write( "version\r\n".getBytes() ); + this.flush(); + String response = this.readLine(); + } + catch ( IOException ex ) { + return false; + } + + return true; + } + + /** + * reads a line + * intentionally not using the deprecated readLine method from DataInputStream + * + * @return String that was read in + * @throws IOException if io problems during read + */ + public String readLine() throws IOException { + if ( sock == null || !sock.isConnected() ) { + log.error( "++++ attempting to read from closed socket" ); + throw new IOException( "++++ attempting to read from closed socket" ); + } + + byte[] b = new byte[1]; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + boolean eol = false; + + while ( in.read( b, 0, 1 ) != -1 ) { + + if ( b[0] == 13 ) { + eol = true; + } + else { + if ( eol ) { + if ( b[0] == 10 ) + break; + + eol = false; + } + } + + // cast byte into char array + bos.write( b, 0, 1 ); + } + + if ( bos == null || bos.size() <= 0 ) { + throw new IOException( "++++ Stream appears to be dead, so closing it down" ); + } + + // else return the string + return bos.toString().trim(); + } + + /** + * reads up to end of line and returns nothing + * + * @throws IOException if io problems during read + */ + public void clearEOL() throws IOException { + if ( sock == null || !sock.isConnected() ) { + log.error( "++++ attempting to read from closed socket" ); + throw new IOException( "++++ attempting to read from closed socket" ); + } + + byte[] b = new byte[1]; + boolean eol = false; + while ( in.read( b, 0, 1 ) != -1 ) { + + // only stop when we see + // \r (13) followed by \n (10) + if ( b[0] == 13 ) { + eol = true; + continue; + } + + if ( eol ) { + if ( b[0] == 10 ) + break; + + eol = false; + } + } + } + + /** + * reads length bytes into the passed in byte array from dtream + * + * @param b byte array + * @throws IOException if io problems during read + */ + public int read( byte[] b ) throws IOException { + if ( sock == null || !sock.isConnected() ) { + log.error( "++++ attempting to read from closed socket" ); + throw new IOException( "++++ attempting to read from closed socket" ); + } + + int count = 0; + while ( count < b.length ) { + int cnt = in.read( b, count, (b.length - count) ); + count += cnt; + } + + return count; + } + + /** + * flushes output stream + * + * @throws IOException if io problems during read + */ + void flush() throws IOException { + if ( sock == null || !sock.isConnected() ) { + log.error( "++++ attempting to write to closed socket" ); + throw new IOException( "++++ attempting to write to closed socket" ); + } + out.flush(); + } + + /** + * writes a byte array to the output stream + * + * @param b byte array to write + * @throws IOException if an io error happens + */ + void write( byte[] b ) throws IOException { + if ( sock == null || !sock.isConnected() ) { + log.error( "++++ attempting to write to closed socket" ); + throw new IOException( "++++ attempting to write to closed socket" ); + } + out.write( b ); + } + + /** + * use the sockets hashcode for this object + * so we can key off of SockIOs + * + * @return int hashcode + */ + public int hashCode() { + return ( sock == null ) ? 0 : sock.hashCode(); + } + + /** + * returns the string representation of this socket + * + * @return string + */ + public String toString() { + return ( sock == null ) ? "" : sock.toString(); + } + + /** + * Hack to reap any leaking children. + */ + protected void finalize() throws Throwable { + try { + if ( sock != null ) { + log.error( "++++ closing potentially leaked socket in finalize" ); + sock.close(); + sock = null; + } + } + catch ( Throwable t ) { + log.error( t.getMessage(), t ); + } + finally { + super.finalize(); + } + } + } +} diff --git a/src/main/java/com/meetup/memcached/test/MemcachedBench.java b/src/main/java/com/meetup/memcached/test/MemcachedBench.java new file mode 100644 index 0000000..9f0491f --- /dev/null +++ b/src/main/java/com/meetup/memcached/test/MemcachedBench.java @@ -0,0 +1,99 @@ +/** + * 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 + */ +package com.meetup.memcached.test; + +import com.meetup.memcached.*; +import java.util.*; +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.apache.log4j.BasicConfigurator; + +public class MemcachedBench { + + // logger + private static Logger log = + Logger.getLogger( MemcachedBench.class.getName() ); + + public static void main(String[] args) { + + BasicConfigurator.configure(); + org.apache.log4j.Logger.getRootLogger().setLevel( Level.OFF ); + + int runs = Integer.parseInt(args[0]); + int start = Integer.parseInt(args[1]); + + String[] serverlist = { "192.168.1.50:1624" }; + + // initialize the pool for memcache servers + SockIOPool pool = SockIOPool.getInstance( "test" ); + pool.setServers(serverlist); + + pool.setInitConn( 100 ); + pool.setMinConn( 100 ); + pool.setMaxConn( 500 ); + pool.setMaintSleep( 20 ); + + pool.setNagle( false ); + pool.initialize(); + + // get client instance + MemcachedClient mc = new MemcachedClient( "test" ); + mc.setCompressEnable( false ); + + String keyBase = "testKey"; + String object = "This is a test of an object blah blah es, serialization does not seem to slow things down so much. The gzip compression is horrible horrible performance, so we only use it for very large objects. I have not done any heavy benchmarking recently"; + + long begin = System.currentTimeMillis(); + for (int i = start; i < start+runs; i++) { + mc.set(keyBase + i, object); + } + long end = System.currentTimeMillis(); + long time = end - begin; + System.out.println(runs + " sets: " + time + "ms"); + + begin = System.currentTimeMillis(); + for (int i = start; i < start+runs; i++) { + String str = (String) mc.get(keyBase + i); + } + end = System.currentTimeMillis(); + time = end - begin; + System.out.println(runs + " gets: " + time + "ms"); + + String[] keys = new String[ runs ]; + int j = 0; + for (int i = start; i < start+runs; i++) { + keys[ j ] = keyBase + i; + j++; + } + begin = System.currentTimeMillis(); + Map vals = mc.getMulti( keys ); + end = System.currentTimeMillis(); + time = end - begin; + System.out.println(runs + " getMulti: " + time + "ms"); + + begin = System.currentTimeMillis(); + for (int i = start; i < start+runs; i++) { + mc.delete( keyBase + i ); + } + end = System.currentTimeMillis(); + time = end - begin; + System.out.println(runs + " deletes: " + time + "ms"); + + SockIOPool.getInstance( "test" ).shutDown(); + } +} diff --git a/src/main/java/com/meetup/memcached/test/MemcachedTest.java b/src/main/java/com/meetup/memcached/test/MemcachedTest.java new file mode 100644 index 0000000..f695e96 --- /dev/null +++ b/src/main/java/com/meetup/memcached/test/MemcachedTest.java @@ -0,0 +1,151 @@ +/** + * 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 + */ +package com.meetup.memcached.test; + +import com.meetup.memcached.*; +import java.util.*; + +public class MemcachedTest { + + // store results from threads + private static Hashtable threadInfo = + new Hashtable(); + + /** + * This runs through some simple tests of the MemcacheClient. + * + * Command line args: + * args[0] = number of threads to spawn + * args[1] = number of runs per thread + * args[2] = size of object to store + * + * @param args the command line arguments + */ + public static void main(String[] args) { + + String[] serverlist = { "cache1.int.meetup.com:12345", "cache0.int.meetup.com:12345" }; + + // initialize the pool for memcache servers + SockIOPool pool = SockIOPool.getInstance(); + pool.setServers( serverlist ); + + pool.setInitConn(5); + pool.setMinConn(5); + pool.setMaxConn(50); + pool.setMaintSleep(30); + + pool.setNagle(false); + pool.initialize(); + + int threads = Integer.parseInt(args[0]); + int runs = Integer.parseInt(args[1]); + int size = 1024 * Integer.parseInt(args[2]); // how many kilobytes + + // get object to store + int[] obj = new int[size]; + for (int i = 0; i < size; i++) { + obj[i] = i; + } + + String[] keys = new String[size]; + for (int i = 0; i < size; i++) { + keys[i] = "test_key" + i; + } + + for (int i = 0; i < threads; i++) { + bench b = new bench(runs, i, obj, keys); + b.start(); + } + + int i = 0; + while (i < threads) { + if (threadInfo.containsKey(new Integer(i))) { + System.out.println( threadInfo.get( new Integer( i ) ) ); + i++; + } + else { + try { + Thread.sleep(1000); + } + catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + pool.shutDown(); + System.exit(1); + } + + /** + * Test code per thread. + */ + private static class bench extends Thread { + private int runs; + private int threadNum; + private int[] object; + private String[] keys; + private int size; + + public bench(int runs, int threadNum, int[] object, String[] keys) { + this.runs = runs; + this.threadNum = threadNum; + this.object = object; + this.keys = keys; + this.size = object.length; + } + + public void run() { + + StringBuilder result = new StringBuilder(); + + // get client instance + MemcachedClient mc = new MemcachedClient(); + mc.setCompressEnable(false); + mc.setCompressThreshold(0); + + // time deletes + long start = System.currentTimeMillis(); + for (int i = 0; i < runs; i++) { + mc.delete(keys[i]); + } + long elapse = System.currentTimeMillis() - start; + float avg = (float) elapse / runs; + result.append("\nthread " + threadNum + ": runs: " + runs + " deletes of obj " + (size/1024) + "KB -- avg time per req " + avg + " ms (total: " + elapse + " ms)"); + + // time stores + start = System.currentTimeMillis(); + for (int i = 0; i < runs; i++) { + mc.set(keys[i], object); + } + elapse = System.currentTimeMillis() - start; + avg = (float) elapse / runs; + result.append("\nthread " + threadNum + ": runs: " + runs + " stores of obj " + (size/1024) + "KB -- avg time per req " + avg + " ms (total: " + elapse + " ms)"); + + start = System.currentTimeMillis(); + for (int i = 0; i < runs; i++) { + mc.get(keys[i]); + } + elapse = System.currentTimeMillis() - start; + avg = (float) elapse / runs; + result.append("\nthread " + threadNum + ": runs: " + runs + " gets of obj " + (size/1024) + "KB -- avg time per req " + avg + " ms (total: " + elapse + " ms)"); + + threadInfo.put(new Integer(threadNum), result); + } + } +} diff --git a/src/main/java/com/meetup/memcached/test/TestMemcached.java b/src/main/java/com/meetup/memcached/test/TestMemcached.java new file mode 100644 index 0000000..b0a9bae --- /dev/null +++ b/src/main/java/com/meetup/memcached/test/TestMemcached.java @@ -0,0 +1,63 @@ +/** + * 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 + */ +package com.meetup.memcached.test; + +import com.meetup.memcached.*; +import org.apache.log4j.*; + +public class TestMemcached { + public static void main(String[] args) { + // memcached should be running on port 11211 but NOT on 11212 + + BasicConfigurator.configure(); + String[] servers = { "192.168.1.1:1624", "192.168.1.1:1625" }; + SockIOPool pool = SockIOPool.getInstance(); + pool.setServers( servers ); + pool.setFailover( true ); + pool.setInitConn( 10 ); + pool.setMinConn( 5 ); + pool.setMaxConn( 250 ); + pool.setMaintSleep( 30 ); + pool.setNagle( false ); + pool.setSocketTO( 3000 ); + pool.setAliveCheck( true ); + pool.initialize(); + + MemcachedClient mcc = new MemcachedClient(); + + // turn off most memcached client logging: + com.meetup.memcached.Logger.getLogger( MemcachedClient.class.getName() ).setLevel( com.meetup.memcached.Logger.LEVEL_WARN ); + + for ( int i = 0; i < 10; i++ ) { + boolean success = mcc.set( "" + i, "Hello!" ); + String result = (String)mcc.get( "" + i ); + System.out.println( String.format( "set( %d ): %s", i, success ) ); + System.out.println( String.format( "get( %d ): %s", i, result ) ); + } + + System.out.println( "\n\t -- sleeping --\n" ); + try { Thread.sleep( 10000 ); } catch ( Exception ex ) { } + + for ( int i = 0; i < 10; i++ ) { + boolean success = mcc.set( "" + i, "Hello!" ); + String result = (String)mcc.get( "" + i ); + System.out.println( String.format( "set( %d ): %s", i, success ) ); + System.out.println( String.format( "get( %d ): %s", i, result ) ); + } + } +} diff --git a/src/main/java/com/meetup/memcached/test/UnitTests.java b/src/main/java/com/meetup/memcached/test/UnitTests.java new file mode 100644 index 0000000..f9bc089 --- /dev/null +++ b/src/main/java/com/meetup/memcached/test/UnitTests.java @@ -0,0 +1,403 @@ +/** + * 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 Kevin Burton + * @author greg whalin + */ +package com.meetup.memcached.test; + +import com.meetup.memcached.*; +import java.util.*; +import java.io.Serializable; + +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.apache.log4j.BasicConfigurator; + +public class UnitTests { + + // logger + private static Logger log = + Logger.getLogger( UnitTests.class.getName() ); + + public static MemcachedClient mc = null; + + public static void test1() { + mc.set( "foo", Boolean.TRUE ); + Boolean b = (Boolean)mc.get( "foo" ); + assert b.booleanValue(); + log.error( "+ store/retrieve Boolean type test passed" ); + } + + public static void test2() { + mc.set( "foo", new Integer( Integer.MAX_VALUE ) ); + Integer i = (Integer)mc.get( "foo" ); + assert i.intValue() == Integer.MAX_VALUE; + log.error( "+ store/retrieve Integer type test passed" ); + } + + public static void test3() { + String input = "test of string encoding"; + mc.set( "foo", input ); + String s = (String)mc.get( "foo" ); + assert s.equals( input ); + log.error( "+ store/retrieve String type test passed" ); + } + + public static void test4() { + mc.set( "foo", new Character( 'z' ) ); + Character c = (Character)mc.get( "foo" ); + assert c.charValue() == 'z'; + log.error( "+ store/retrieve Character type test passed" ); + } + + public static void test5() { + mc.set( "foo", new Byte( (byte)127 ) ); + Byte b = (Byte)mc.get( "foo" ); + assert b.byteValue() == 127; + log.error( "+ store/retrieve Byte type test passed" ); + } + + public static void test6() { + mc.set( "foo", new StringBuffer( "hello" ) ); + StringBuffer o = (StringBuffer)mc.get( "foo" ); + assert o.toString().equals( "hello" ); + log.error( "+ store/retrieve StringBuffer type test passed" ); + } + + public static void test7() { + mc.set( "foo", new Short( (short)100 ) ); + Short o = (Short)mc.get( "foo" ); + assert o.shortValue() == 100; + log.error( "+ store/retrieve Short type test passed" ); + } + + public static void test8() { + mc.set( "foo", new Long( Long.MAX_VALUE ) ); + Long o = (Long)mc.get( "foo" ); + assert o.longValue() == Long.MAX_VALUE; + log.error( "+ store/retrieve Long type test passed" ); + } + + public static void test9() { + mc.set( "foo", new Double( 1.1 ) ); + Double o = (Double)mc.get( "foo" ); + assert o.doubleValue() == 1.1; + log.error( "+ store/retrieve Double type test passed" ); + } + + public static void test10() { + mc.set( "foo", new Float( 1.1f ) ); + Float o = (Float)mc.get( "foo" ); + assert o.floatValue() == 1.1f; + log.error( "+ store/retrieve Float type test passed" ); + } + + public static void test11() { + mc.set( "foo", new Integer( 100 ), new Date( System.currentTimeMillis() )); + try { Thread.sleep( 1000 ); } catch ( Exception ex ) { } + assert mc.get( "foo" ) == null; + log.error( "+ store/retrieve w/ expiration test passed" ); + } + + public static void test12() { + long i = 0; + mc.storeCounter("foo", i); + mc.incr("foo"); // foo now == 1 + mc.incr("foo", (long)5); // foo now == 6 + long j = mc.decr("foo", (long)2); // foo now == 4 + assert j == 4; + assert j == mc.getCounter( "foo" ); + log.error( "+ incr/decr test passed" ); + } + + public static void test13() { + Date d1 = new Date(); + mc.set("foo", d1); + Date d2 = (Date) mc.get("foo"); + assert d1.equals( d2 ); + log.error( "+ store/retrieve Date type test passed" ); + } + + public static void test14() { + assert !mc.keyExists( "foobar123" ); + mc.set( "foobar123", new Integer( 100000) ); + assert mc.keyExists( "foobar123" ); + log.error( "+ store/retrieve test passed" ); + + assert !mc.keyExists( "counterTest123" ); + mc.storeCounter( "counterTest123", 0 ); + assert mc.keyExists( "counterTest123" ); + log.error( "+ counter store test passed" ); + } + + public static void test15() { + + Map stats = mc.statsItems(); + assert stats != null; + + stats = mc.statsSlabs(); + assert stats != null; + + log.error( "+ stats test passed" ); + } + + public static void test16() { + assert !mc.set( "foo", null ); + log.error( "+ invalid data store [null] test passed" ); + } + + public static void test17() { + mc.set( "foo bar", Boolean.TRUE ); + Boolean b = (Boolean)mc.get( "foo bar" ); + assert b.booleanValue(); + log.error( "+ store/retrieve Boolean type test passed" ); + } + + public static void test18() { + long i = 0; + mc.addOrIncr( "foo" ); // foo now == 0 + mc.incr( "foo" ); // foo now == 1 + mc.incr( "foo", (long)5 ); // foo now == 6 + + mc.addOrIncr( "foo" ); // foo now 7 + + long j = mc.decr( "foo", (long)3 ); // foo now == 4 + assert j == 4; + assert j == mc.getCounter( "foo" ); + + log.error( "+ incr/decr test passed" ); + } + + public static void test19() { + int max = 100; + String[] keys = new String[ max ]; + for ( int i=0; i results = mc.getMulti( keys ); + for ( int i=0; i results = mc.getMulti( keys ); + for ( int i=0; i results = mc.getMulti( allKeys ); + + assert allKeys.length == results.size(); + for ( String key : setKeys ) { + String val = (String)results.get( key ); + assert key.equals( val ); + } + + log.error( "+ getMulti w/ keys that don't exist test passed" ); + } + + public static void runAlTests( MemcachedClient mc ) { + test14(); + for ( int t = 0; t < 2; t++ ) { + mc.setCompressEnable( ( t&1 ) == 1 ); + + test1(); + test2(); + test3(); + test4(); + test5(); + test6(); + test7(); + test8(); + test9(); + test10(); + test11(); + test12(); + test13(); + test15(); + test16(); + test17(); + test21(); + test22(); + test23(); + test24(); + + for ( int i = 0; i < 3; i++ ) + test19(); + + test20( 8191, 1, 0 ); + test20( 8192, 1, 0 ); + test20( 8193, 1, 0 ); + + test20( 16384, 100, 0 ); + test20( 17000, 128, 0 ); + + test20( 128*1024, 1023, 0 ); + test20( 128*1024, 1023, 1 ); + test20( 128*1024, 1024, 0 ); + test20( 128*1024, 1024, 1 ); + + test20( 128*1024, 1023, 0 ); + test20( 128*1024, 1023, 1 ); + test20( 128*1024, 1024, 0 ); + test20( 128*1024, 1024, 1 ); + + test20( 900*1024, 32*1024, 0 ); + test20( 900*1024, 32*1024, 1 ); + } + + } + + /** + * This runs through some simple tests of the MemcacheClient. + * + * Command line args: + * args[0] = number of threads to spawn + * args[1] = number of runs per thread + * args[2] = size of object to store + * + * @param args the command line arguments + */ + public static void main(String[] args) { + + BasicConfigurator.configure(); + org.apache.log4j.Logger.getRootLogger().setLevel( Level.WARN ); + + if ( !UnitTests.class.desiredAssertionStatus() ) { + System.err.println( "WARNING: assertions are disabled!" ); + try { Thread.sleep( 3000 ); } catch ( InterruptedException e ) {} + } + + String[] serverlist = { + "192.168.1.50:1620", + "192.168.1.50:1621", + "192.168.1.50:1622", + "192.168.1.50:1623", + "192.168.1.50:1624", + "192.168.1.50:1625", + "192.168.1.50:1626", + "192.168.1.50:1627", + "192.168.1.50:1628", + "192.168.1.50:1629" + }; + + Integer[] weights = { 1, 1, 1, 1, 10, 5, 1, 1, 1, 3 }; + + if ( args.length > 0 ) + serverlist = args; + + // initialize the pool for memcache servers + SockIOPool pool = SockIOPool.getInstance( "test" ); + pool.setServers( serverlist ); + pool.setWeights( weights ); + pool.setMaxConn( 250 ); + pool.setNagle( false ); + pool.setHashingAlg( SockIOPool.CONSISTENT_HASH ); + pool.initialize(); + + mc = new MemcachedClient( "test" ); + runAlTests( mc ); + } + + /** + * Class for testing serializing of objects. + * + * @author $Author: $ + * @version $Revision: $ $Date: $ + */ + public static final class TestClass implements Serializable { + + private String field1; + private String field2; + private Integer field3; + + public TestClass( String field1, String field2, Integer field3 ) { + this.field1 = field1; + this.field2 = field2; + this.field3 = field3; + } + + public String getField1() { return this.field1; } + public String getField2() { return this.field2; } + public Integer getField3() { return this.field3; } + + public boolean equals( Object o ) { + if ( this == o ) return true; + if ( !( o instanceof TestClass ) ) return false; + + TestClass obj = (TestClass)o; + + return ( ( this.field1 == obj.getField1() || ( this.field1 != null && this.field1.equals( obj.getField1() ) ) ) + && ( this.field2 == obj.getField2() || ( this.field2 != null && this.field2.equals( obj.getField2() ) ) ) + && ( this.field3 == obj.getField3() || ( this.field3 != null && this.field3.equals( obj.getField3() ) ) ) ); + } + } +}