// Copyright (c) 2006 Dustin Sallings <dustin@spy.net>
package net.spy.memcached;
import java.io.IOException;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import net.spy.memcached.compat.SpyObject;
import net.spy.memcached.compat.log.LoggerFactory;
import net.spy.memcached.ops.KeyedOperation;
import net.spy.memcached.ops.Operation;
import net.spy.memcached.ops.OperationException;
import net.spy.memcached.ops.OperationState;
import net.spy.memcached.ops.VBucketAware;
import net.spy.memcached.vbucket.VBucketNodeLocator;
import net.spy.memcached.vbucket.Reconfigurable;
import net.spy.memcached.vbucket.config.Bucket;
/**
* Connection to a cluster of memcached servers.
*/
public final class MemcachedConnection extends SpyObject implements Reconfigurable {
// The number of empty selects we'll allow before assuming we may have
// missed one and should check the current selectors. This generally
// indicates a bug, but we'll check it nonetheless.
private static final int DOUBLE_CHECK_EMPTY = 256;
// The number of empty selects we'll allow before blowing up. It's too
// easy to write a bug that causes it to loop uncontrollably. This helps
// find those bugs and often works around them.
private static final int EXCESSIVE_EMPTY = 0x1000000;
private volatile boolean shutDown=false;
// If true, optimization will collapse multiple sequential get ops
private final boolean shouldOptimize;
private Selector selector=null;
private final NodeLocator locator;
private final FailureMode failureMode;
// maximum amount of time to wait between reconnect attempts
private final long maxDelay;
private int emptySelects=0;
private final int bufSize;
private final ConnectionFactory connectionFactory;
// AddedQueue is used to track the QueueAttachments for which operations
// have recently been queued.
private final ConcurrentLinkedQueue<MemcachedNode> addedQueue;
// reconnectQueue contains the attachments that need to be reconnected
// The key is the time at which they are eligible for reconnect
private final SortedMap<Long, MemcachedNode> reconnectQueue;
private final Collection<ConnectionObserver> connObservers =
new ConcurrentLinkedQueue<ConnectionObserver>();
private final OperationFactory opFact;
private final int timeoutExceptionThreshold;
private final Collection<Operation> retryOps;
private final ConcurrentLinkedQueue<MemcachedNode> nodesToShutdown;
/**
* Construct a memcached connection.
*
* @param bufSize the size of the buffer used for reading from the server
* @param f the factory that will provide an operation queue
* @param a the addresses of the servers to connect to
*
* @throws IOException if a connection attempt fails early
*/
public MemcachedConnection(int bufSize, ConnectionFactory f,
List<InetSocketAddress> a, Collection<ConnectionObserver> obs,
FailureMode fm, OperationFactory opfactory)
throws IOException {
connObservers.addAll(obs);
reconnectQueue=new TreeMap<Long, MemcachedNode>();
addedQueue=new ConcurrentLinkedQueue<MemcachedNode>();
failureMode = fm;
shouldOptimize = f.shouldOptimize();
maxDelay = f.getMaxReconnectDelay();
opFact = opfactory;
timeoutExceptionThreshold = f.getTimeoutExceptionThreshold();
selector=Selector.open();
retryOps = new ArrayList<Operation>();
nodesToShutdown = new ConcurrentLinkedQueue<MemcachedNode>();
this.bufSize = bufSize;
this.connectionFactory = f;
List<MemcachedNode> connections = createConnections(a);
locator=f.createLocator(connections);
}
private List<MemcachedNode> createConnections(final Collection<InetSocketAddress> a)
throws IOException {
List<MemcachedNode> connections=new ArrayList<MemcachedNode>(a.size());
for(SocketAddress sa : a) {
SocketChannel ch=SocketChannel.open();
ch.configureBlocking(false);
MemcachedNode qa=this.connectionFactory.createMemcachedNode(sa, ch, bufSize);
int ops=0;
ch.socket().setTcpNoDelay(!this.connectionFactory.useNagleAlgorithm());
// Initially I had attempted to skirt this by queueing every
// connect, but it considerably slowed down start time.
try {
if(ch.connect(sa)) {
getLogger().info("Connected to %s immediately", qa);
connected(qa);
} else {
getLogger().info("Added %s to connect queue", qa);
ops=SelectionKey.OP_CONNECT;
}
qa.setSk(ch.register(selector, ops, qa));
assert ch.isConnected()
|| qa.getSk().interestOps() == SelectionKey.OP_CONNECT
: "Not connected, and not wanting to connect";
} catch(SocketException e) {
getLogger().warn("Socket error on initial connect", e);
queueReconnect(qa);
}
connections.add(qa);
}
return connections;
}
public void reconfigure(Bucket bucket) {
try {
if (!(locator instanceof VBucketNodeLocator)) {
return;
}
// get a new collection of addresses from the received config
List<String> servers = bucket.getConfig().getServers();
HashSet<SocketAddress> newServerAddresses = new HashSet<SocketAddress>();
ArrayList<InetSocketAddress> newServers = new ArrayList<InetSocketAddress>();
for (String server : servers) {
int finalColon = server.lastIndexOf(':');
if (finalColon < 1) {
throw new IllegalArgumentException("Invalid server ``"
+ server + "'' in vbucket's server list");
}
String hostPart = server.substring(0, finalColon);
String portNum = server.substring(finalColon + 1);
InetSocketAddress address = new InetSocketAddress(hostPart,
Integer.parseInt(portNum));
// add parsed address to our collections
newServerAddresses.add(address);
newServers.add(address);
}
// split current nodes to "odd nodes" and "stay nodes"
ArrayList<MemcachedNode> oddNodes = new ArrayList<MemcachedNode>();
ArrayList<MemcachedNode> stayNodes = new ArrayList<MemcachedNode>();
ArrayList<InetSocketAddress> stayServers = new ArrayList<InetSocketAddress>();
for (MemcachedNode current : locator.getAll()) {
if (newServerAddresses.contains(current.getSocketAddress())) {
stayNodes.add(current);
stayServers.add((InetSocketAddress) current.getSocketAddress());
} else {
oddNodes.add(current);
}
}
// prepare a collection of addresses for new nodes
newServers.removeAll(stayServers);
// create a collection of new nodes
List<MemcachedNode> newNodes = createConnections(newServers);
// merge stay nodes with new nodes
List<MemcachedNode> mergedNodes = new ArrayList<MemcachedNode>();
mergedNodes.addAll(stayNodes);
mergedNodes.addAll(newNodes);
// call update locator with new nodes list and vbucket config
((VBucketNodeLocator) locator).updateLocator(mergedNodes, bucket.getConfig());
// schedule shutdown for the oddNodes
nodesToShutdown.addAll(oddNodes);
} catch (IOException e) {
getLogger().error("Connection reconfiguration failed", e);
}
}
private boolean selectorsMakeSense() {
for(MemcachedNode qa : locator.getAll()) {
if(qa.getSk() != null && qa.getSk().isValid()) {
if(qa.getChannel().isConnected()) {
int sops=qa.getSk().interestOps();
int expected=0;
if(qa.hasReadOp()) {
expected |= SelectionKey.OP_READ;
}
if(qa.hasWriteOp()) {
expected |= SelectionKey.OP_WRITE;
}
if(qa.getBytesRemainingToWrite() > 0) {
expected |= SelectionKey.OP_WRITE;
}
assert sops == expected : "Invalid ops: "
+ qa + ", expected " + expected + ", got " + sops;
} else {
int sops=qa.getSk().interestOps();
assert sops == SelectionKey.OP_CONNECT
: "Not connected, and not watching for connect: "
+ sops;
}
}
}
getLogger().debug("Checked the selectors.");
return true;
}
/**
* MemcachedClient calls this method to handle IO over the connections.
*/
public void handleIO() throws IOException {
if(shutDown) {
throw new IOException("No IO while shut down");
}
// Deal with all of the stuff that's been added, but may not be marked
// writable.
handleInputQueue();
getLogger().debug("Done dealing with queue.");
long delay=0;
if(!reconnectQueue.isEmpty()) {
long now=System.currentTimeMillis();
long then=reconnectQueue.firstKey();
delay=Math.max(then-now, 1);
}
getLogger().debug("Selecting with delay of %sms", delay);
assert selectorsMakeSense() : "Selectors don't make sense.";
int selected=selector.select(delay);
Set<SelectionKey> selectedKeys=selector.selectedKeys();
if(selectedKeys.isEmpty() && !shutDown) {
getLogger().debug("No selectors ready, interrupted: "
+ Thread.interrupted());
if(++emptySelects > DOUBLE_CHECK_EMPTY) {
for(SelectionKey sk : selector.keys()) {
getLogger().info("%s has %s, interested in %s",
sk, sk.readyOps(), sk.interestOps());
if(sk.readyOps() != 0) {
getLogger().info("%s has a ready op, handling IO", sk);
handleIO(sk);
} else {
lostConnection((MemcachedNode)sk.attachment());
}
}
assert emptySelects < EXCESSIVE_EMPTY
: "Too many empty selects";
}
} else {
getLogger().debug("Selected %d, selected %d keys",
selected, selectedKeys.size());
emptySelects=0;
for(SelectionKey sk : selectedKeys) {
handleIO(sk);
}
selectedKeys.clear();
}
// see if any connections blew up with large number of timeouts
for(SelectionKey sk : selector.keys()) {
MemcachedNode mn = (MemcachedNode)sk.attachment();
if (mn.getContinuousTimeout() > timeoutExceptionThreshold)
{
getLogger().warn("%s exceeded continuous timeout threshold", sk);
lostConnection(mn);
}
}
if(!shutDown && !reconnectQueue.isEmpty()) {
attemptReconnects();
}
// rehash operations that in retry state
redistributeOperations(retryOps);
retryOps.clear();
// try to shutdown odd nodes
for (MemcachedNode qa : nodesToShutdown) {
if (!addedQueue.contains(qa)) {
nodesToShutdown.remove(qa);
Collection<Operation> notCompletedOperations = qa.destroyInputQueue();
if (qa.getChannel() != null) {
qa.getChannel().close();
qa.setSk(null);
if (qa.getBytesRemainingToWrite() > 0) {
getLogger().warn(
"Shut down with %d bytes remaining to write",
qa.getBytesRemainingToWrite());
}
getLogger().debug("Shut down channel %s", qa.getChannel());
}
redistributeOperations(notCompletedOperations);
}
}
}
// Handle any requests that have been made against the client.
private void handleInputQueue() {
if(!addedQueue.isEmpty()) {
getLogger().debug("Handling queue");
// If there's stuff in the added queue. Try to process it.
Collection<MemcachedNode> toAdd=new HashSet<MemcachedNode>();
// Transfer the queue into a hashset. There are very likely more
// additions than there are nodes.
Collection<MemcachedNode> todo=new HashSet<MemcachedNode>();
try {
MemcachedNode qa=null;
while((qa=addedQueue.remove()) != null) {
todo.add(qa);
}
} catch(NoSuchElementException e) {
// Found everything
}
// Now process the queue.
for(MemcachedNode qa : todo) {
boolean readyForIO=false;
if(qa.isActive()) {
if(qa.getCurrentWriteOp() != null) {
readyForIO=true;
getLogger().debug("Handling queued write %s", qa);
}
} else {
toAdd.add(qa);
}
qa.copyInputQueue();
if(readyForIO) {
try {
if(qa.getWbuf().hasRemaining()) {
handleWrites(qa.getSk(), qa);
}
} catch(IOException e) {
getLogger().warn("Exception handling write", e);
lostConnection(qa);
}
}
qa.fixupOps();
}
addedQueue.addAll(toAdd);
}
}
/**
* Add a connection observer.
*
* @return whether the observer was successfully added
*/
public boolean addObserver(ConnectionObserver obs) {
return connObservers.add(obs);
}
/**
* Remove a connection observer.
*
* @return true if the observer existed and now doesn't
*/
public boolean removeObserver(ConnectionObserver obs) {
return connObservers.remove(obs);
}
private void connected(MemcachedNode qa) {
assert qa.getChannel().isConnected() : "Not connected.";
int rt = qa.getReconnectCount();
qa.connected();
for(ConnectionObserver observer : connObservers) {
observer.connectionEstablished(qa.getSocketAddress(), rt);
}
}
private void lostConnection(MemcachedNode qa) {
queueReconnect(qa);
for(ConnectionObserver observer : connObservers) {
observer.connectionLost(qa.getSocketAddress());
}
}
// Handle IO for a specific selector. Any IOException will cause a
// reconnect
private void handleIO(SelectionKey sk) {
MemcachedNode qa=(MemcachedNode)sk.attachment();
try {
getLogger().debug(
"Handling IO for: %s (r=%s, w=%s, c=%s, op=%s)",
sk, sk.isReadable(), sk.isWritable(),
sk.isConnectable(), sk.attachment());
if(sk.isConnectable()) {
getLogger().info("Connection state changed for %s", sk);
final SocketChannel channel=qa.getChannel();
if(channel.finishConnect()) {
connected(qa);
addedQueue.offer(qa);
if(qa.getWbuf().hasRemaining()) {
handleWrites(sk, qa);
}
} else {
assert !channel.isConnected() : "connected";
}
} else {
if(sk.isReadable()) {
handleReads(sk, qa);
}
if(sk.isWritable()) {
handleWrites(sk, qa);
}
}
} catch(ClosedChannelException e) {
// Note, not all channel closes end up here
if(!shutDown) {
getLogger().info("Closed channel and not shutting down. "
+ "Queueing reconnect on %s", qa, e);
lostConnection(qa);
}
} catch(ConnectException e) {
// Failures to establish a connection should attempt a reconnect
// without signaling the observers.
getLogger().info("Reconnecting due to failure to connect to %s",
qa, e);
queueReconnect(qa);
} catch (OperationException e) {
qa.setupForAuth(); // noop if !shouldAuth
getLogger().info("Reconnection due to exception " +
"handling a memcached operation on %s. " +
"This may be due to an authentication failure.", qa, e);
lostConnection(qa);
} catch(Exception e) {
// Any particular error processing an item should simply
// cause us to reconnect to the server.
//
// One cause is just network oddness or servers
// restarting, which lead here with IOException
qa.setupForAuth(); // noop if !shouldAuth
getLogger().info("Reconnecting due to exception on %s", qa, e);
lostConnection(qa);
}
qa.fixupOps();
}
private void handleWrites(SelectionKey sk, MemcachedNode qa)
throws IOException {
qa.fillWriteBuffer(shouldOptimize);
boolean canWriteMore=qa.getBytesRemainingToWrite() > 0;
while(canWriteMore) {
int wrote=qa.writeSome();
qa.fillWriteBuffer(shouldOptimize);
canWriteMore = wrote > 0 && qa.getBytesRemainingToWrite() > 0;
}
}
private void handleReads(SelectionKey sk, MemcachedNode qa)
throws IOException {
Operation currentOp = qa.getCurrentReadOp();
ByteBuffer rbuf=qa.getRbuf();
final SocketChannel channel = qa.getChannel();
int read=channel.read(rbuf);
if (read < 0) {
// our model is to keep the connection alive for future ops
// so we'll queue a reconnect if disconnected via an IOException
throw new IOException("Disconnected unexpected, will reconnect.");
}
while(read > 0) {
getLogger().debug("Read %d bytes", read);
rbuf.flip();
while(rbuf.remaining() > 0) {
if(currentOp == null) {
throw new IllegalStateException("No read operation.");
}
currentOp.readFromBuffer(rbuf);
if(currentOp.getState() == OperationState.COMPLETE) {
getLogger().debug(
"Completed read op: %s and giving the next %d bytes",
currentOp, rbuf.remaining());
Operation op=qa.removeCurrentReadOp();
assert op == currentOp
: "Expected to pop " + currentOp + " got " + op;
currentOp=qa.getCurrentReadOp();
} else if (currentOp.getState() == OperationState.RETRY) {
getLogger().debug(
"Reschedule read op due to NOT_MY_VBUCKET error: %s ",
currentOp);
((VBucketAware) currentOp).addNotMyVbucketNode(currentOp.getHandlingNode());
Operation op=qa.removeCurrentReadOp();
assert op == currentOp
: "Expected to pop " + currentOp + " got " + op;
retryOps.add(currentOp);
currentOp=qa.getCurrentReadOp();
}
}
rbuf.clear();
read=channel.read(rbuf);
}
}
// Make a debug string out of the given buffer's values
static String dbgBuffer(ByteBuffer b, int size) {
StringBuilder sb=new StringBuilder();
byte[] bytes=b.array();
for(int i=0; i<size; i++) {
char ch=(char)bytes[i];
if(Character.isWhitespace(ch) || Character.isLetterOrDigit(ch)) {
sb.append(ch);
} else {
sb.append("\\x");
sb.append(Integer.toHexString(bytes[i] & 0xff));
}
}
return sb.toString();
}
private void queueReconnect(MemcachedNode qa) {
if(!shutDown) {
getLogger().warn("Closing, and reopening %s, attempt %d.",
qa, qa.getReconnectCount());
if(qa.getSk() != null) {
qa.getSk().cancel();
assert !qa.getSk().isValid() : "Cancelled selection key is valid";
}
qa.reconnecting();
try {
if(qa.getChannel() != null && qa.getChannel().socket() != null) {
qa.getChannel().socket().close();
} else {
getLogger().info("The channel or socket was null for %s",
qa);
}
} catch(IOException e) {
getLogger().warn("IOException trying to close a socket", e);
}
qa.setChannel(null);
long delay = (long)Math.min(maxDelay,
Math.pow(2, qa.getReconnectCount())) * 1000;
long reconTime = System.currentTimeMillis() + delay;
// Avoid potential condition where two connections are scheduled
// for reconnect at the exact same time. This is expected to be
// a rare situation.
while(reconnectQueue.containsKey(reconTime)) {
reconTime++;
}
reconnectQueue.put(reconTime, qa);
// Need to do a little queue management.
qa.setupResend();
if(failureMode == FailureMode.Redistribute) {
redistributeOperations(qa.destroyInputQueue());
} else if(failureMode == FailureMode.Cancel) {
cancelOperations(qa.destroyInputQueue());
}
}
}
private void cancelOperations(Collection<Operation> ops) {
for(Operation op : ops) {
op.cancel();
}
}
private void redistributeOperations(Collection<Operation> ops) {
for(Operation op : ops) {
if(op instanceof KeyedOperation) {
KeyedOperation ko = (KeyedOperation)op;
int added = 0;
for(String k : ko.getKeys()) {
for(Operation newop : opFact.clone(ko)) {
addOperation(k, newop);
added++;
}
}
assert added > 0
: "Didn't add any new operations when redistributing";
} else {
// Cancel things that don't have definite targets.
op.cancel();
}
}
}
private void attemptReconnects() throws IOException {
final long now=System.currentTimeMillis();
final Map<MemcachedNode, Boolean> seen=
new IdentityHashMap<MemcachedNode, Boolean>();
final List<MemcachedNode> rereQueue=new ArrayList<MemcachedNode>();
SocketChannel ch = null;
for(Iterator<MemcachedNode> i=
reconnectQueue.headMap(now).values().iterator(); i.hasNext();) {
final MemcachedNode qa=i.next();
i.remove();
try {
if(!seen.containsKey(qa)) {
seen.put(qa, Boolean.TRUE);
getLogger().info("Reconnecting %s", qa);
ch=SocketChannel.open();
ch.configureBlocking(false);
int ops=0;
if(ch.connect(qa.getSocketAddress())) {
getLogger().info("Immediately reconnected to %s", qa);
assert ch.isConnected();
} else {
ops=SelectionKey.OP_CONNECT;
}
qa.registerChannel(ch, ch.register(selector, ops, qa));
assert qa.getChannel() == ch : "Channel was lost.";
} else {
getLogger().debug(
"Skipping duplicate reconnect request for %s", qa);
}
} catch(SocketException e) {
getLogger().warn("Error on reconnect", e);
rereQueue.add(qa);
}
catch (Exception e) {
getLogger().error("Exception on reconnect, lost node %s", qa, e);
} finally {
//it's possible that above code will leak file descriptors under abnormal
//conditions (when ch.open() fails and throws IOException.
//always close non connected channel
if (ch != null && !ch.isConnected()
&& !ch.isConnectionPending()) {
try {
ch.close();
} catch (IOException x) {
getLogger().error("Exception closing channel: %s", qa, x);
}
}
}
}
// Requeue any fast-failed connects.
for(MemcachedNode n : rereQueue) {
queueReconnect(n);
}
}
/**
* Get the node locator used by this connection.
*/
NodeLocator getLocator() {
return locator;
}
/**
* Add an operation to the given connection.
*
* @param key the key the operation is operating upon
* @param o the operation
*/
public void addOperation(final String key, final Operation o) {
MemcachedNode placeIn=null;
MemcachedNode primary = locator.getPrimary(key);
if(primary.isActive() || failureMode == FailureMode.Retry) {
placeIn=primary;
} else if(failureMode == FailureMode.Cancel) {
o.cancel();
} else {
// Look for another node in sequence that is ready.
for(Iterator<MemcachedNode> i=locator.getSequence(key);
placeIn == null && i.hasNext(); ) {
MemcachedNode n=i.next();
if(n.isActive()) {
placeIn=n;
}
}
// If we didn't find an active node, queue it in the primary node
// and wait for it to come back online.
if(placeIn == null) {
placeIn = primary;
this.getLogger().warn("Could not redistribute " +
"to another node, retrying primary node for %s.", key);
}
}
assert o.isCancelled() || placeIn != null
: "No node found for key " + key;
if(placeIn != null) {
// add the vbucketIndex to the operation
if (locator instanceof VBucketNodeLocator) {
VBucketNodeLocator vbucketLocator = (VBucketNodeLocator) locator;
int vbucketIndex = vbucketLocator.getVBucketIndex(key);
if (o instanceof VBucketAware) {
VBucketAware vbucketAwareOp = (VBucketAware) o;
vbucketAwareOp.setVBucket(vbucketIndex);
if (!vbucketAwareOp.getNotMyVbucketNodes().isEmpty()) {
MemcachedNode alternative = vbucketLocator.
getAlternative(key, vbucketAwareOp.getNotMyVbucketNodes());
if (alternative != null) {
placeIn = alternative;
}
}
}
}
addOperation(placeIn, o);
} else {
assert o.isCancelled() : "No node found for "
+ key + " (and not immediately cancelled)";
}
}
public void insertOperation(final MemcachedNode node, final Operation o) {
o.setHandlingNode(node);
o.initialize();
node.insertOp(o);
addedQueue.offer(node);
Selector s=selector.wakeup();
assert s == selector : "Wakeup returned the wrong selector.";
getLogger().debug("Added %s to %s", o, node);
}
public void addOperation(final MemcachedNode node, final Operation o) {
o.setHandlingNode(node);
o.initialize();
node.addOp(o);
addedQueue.offer(node);
Selector s=selector.wakeup();
assert s == selector : "Wakeup returned the wrong selector.";
getLogger().debug("Added %s to %s", o, node);
}
public void addOperations(final Map<MemcachedNode, Operation> ops) {
for(Map.Entry<MemcachedNode, Operation> me : ops.entrySet()) {
final MemcachedNode node=me.getKey();
Operation o=me.getValue();
o.setHandlingNode(node);
o.initialize();
node.addOp(o);
addedQueue.offer(node);
}
Selector s=selector.wakeup();
assert s == selector : "Wakeup returned the wrong selector.";
}
/**
* Broadcast an operation to all nodes.
*/
public CountDownLatch broadcastOperation(BroadcastOpFactory of) {
return broadcastOperation(of, locator.getAll());
}
/**
* Broadcast an operation to a specific collection of nodes.
*/
public CountDownLatch broadcastOperation(final BroadcastOpFactory of,
Collection<MemcachedNode> nodes) {
final CountDownLatch latch=new CountDownLatch(locator.getAll().size());
for(MemcachedNode node : nodes) {
Operation op = of.newOp(node, latch);
op.initialize();
node.addOp(op);
op.setHandlingNode(node);
addedQueue.offer(node);
}
Selector s=selector.wakeup();
assert s == selector : "Wakeup returned the wrong selector.";
return latch;
}
/**
* Shut down all of the connections.
*/
public void shutdown() throws IOException {
shutDown=true;
Selector s=selector.wakeup();
assert s == selector : "Wakeup returned the wrong selector.";
for(MemcachedNode qa : locator.getAll()) {
if(qa.getChannel() != null) {
qa.getChannel().close();
qa.setSk(null);
if(qa.getBytesRemainingToWrite() > 0) {
getLogger().warn(
"Shut down with %d bytes remaining to write",
qa.getBytesRemainingToWrite());
}
getLogger().debug("Shut down channel %s", qa.getChannel());
}
}
selector.close();
getLogger().debug("Shut down selector %s", selector);
}
@Override
public String toString() {
StringBuilder sb=new StringBuilder();
sb.append("{MemcachedConnection to");
for(MemcachedNode qa : locator.getAll()) {
sb.append(" ");
sb.append(qa.getSocketAddress());
}
sb.append("}");
return sb.toString();
}
/**
* helper method: increase timeout count on node attached to this op
*
* @param op
*/
public static void opTimedOut(Operation op) {
MemcachedConnection.setTimeout(op, true);
}
/**
* helper method: reset timeout counter
*
* @param op
*/
public static void opSucceeded(Operation op) {
MemcachedConnection.setTimeout(op, false);
}
/**
* helper method: do some error checking and set timeout boolean
*
* @param op
* @param isTimeout
*/
private static void setTimeout(Operation op, boolean isTimeout) {
try {
if (op == null || op.isTimedOutUnsent()) {
return; // op may be null in some cases, e.g. flush
}
MemcachedNode node = op.getHandlingNode();
if (node == null) {
LoggerFactory.getLogger(MemcachedConnection.class).warn("handling node for operation is not set");
}
else {
node.setContinuousTimeout(isTimeout);
}
} catch (Exception e) {
LoggerFactory.getLogger(MemcachedConnection.class).error(e.getMessage());
}
}
}