/* * Copyright 2008-2009 LinkedIn, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy of * the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. */ package voldemort.client.protocol.admin; import java.io.DataInputStream; import java.io.IOException; import java.io.OutputStream; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketException; import java.util.Date; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; import org.apache.log4j.Logger; import voldemort.VoldemortException; import voldemort.client.protocol.RequestFormatType; import voldemort.store.socket.SocketDestination; import voldemort.utils.ByteUtils; import voldemort.utils.Time; import voldemort.utils.pool.KeyedResourcePool; import voldemort.utils.pool.ResourceFactory; /** * A Factory for creating sockets * * */ public class SocketResourceFactory implements ResourceFactory<SocketDestination, SocketAndStreams> { public static final Logger logger = Logger.getLogger(SocketResourceFactory.class); private final int soTimeoutMs; private final int socketBufferSize; private final AtomicInteger created; private final AtomicInteger destroyed; private final boolean socketKeepAlive; private final Map<SocketDestination, Long> lastClosedTimestamps; public SocketResourceFactory(int soTimeoutMs, int socketBufferSize) { this(soTimeoutMs, socketBufferSize, false); } public SocketResourceFactory(int soTimeoutMs, int socketBufferSize, boolean socketKeepAlive) { this.soTimeoutMs = soTimeoutMs; this.created = new AtomicInteger(0); this.destroyed = new AtomicInteger(0); this.socketBufferSize = socketBufferSize; this.socketKeepAlive = socketKeepAlive; this.lastClosedTimestamps = new ConcurrentHashMap<SocketDestination, Long>(); } /** * Close the socket */ public void destroy(SocketDestination dest, SocketAndStreams sands) throws Exception { sands.getSocket().close(); int numDestroyed = destroyed.incrementAndGet(); if(logger.isDebugEnabled()) logger.debug("Destroyed socket " + numDestroyed + " connection to " + dest.getHost() + ":" + dest.getPort()); } /** * Create a socket for the given host/port */ @Override public void createAsync(SocketDestination dest, KeyedResourcePool<SocketDestination, SocketAndStreams> pool) throws Exception { Socket socket = new Socket(); socket.setReceiveBufferSize(this.socketBufferSize); socket.setSendBufferSize(this.socketBufferSize); socket.setTcpNoDelay(true); socket.setSoTimeout(soTimeoutMs); socket.setKeepAlive(this.socketKeepAlive); socket.connect(new InetSocketAddress(dest.getHost(), dest.getPort()), soTimeoutMs); recordSocketCreation(dest, socket); SocketAndStreams sands = new SocketAndStreams(socket, dest.getRequestFormatType()); negotiateProtocol(sands, dest.getRequestFormatType()); pool.checkin(dest, sands); } private void negotiateProtocol(SocketAndStreams socket, RequestFormatType type) throws IOException { OutputStream outputStream = socket.getOutputStream(); byte[] proposal = ByteUtils.getBytes(type.getCode(), "UTF-8"); outputStream.write(proposal); outputStream.flush(); DataInputStream inputStream = socket.getInputStream(); byte[] responseBytes = new byte[2]; inputStream.readFully(responseBytes); String response = ByteUtils.getString(responseBytes, "UTF-8"); if(response.equals("ok")) return; else if(response.equals("no")) throw new VoldemortException(type.getDisplayName() + " is not an acceptable protocol for the server."); else throw new VoldemortException("Unknown server response: " + response); } /* Log relevant socket creation details */ private void recordSocketCreation(SocketDestination dest, Socket socket) throws SocketException { int numCreated = created.incrementAndGet(); logger.debug("Created socket " + numCreated + " for " + dest.getHost() + ":" + dest.getPort() + " using protocol " + dest.getRequestFormatType().getCode()); // check buffer sizes--you often don't get out what you put in! int sendBufferSize = socket.getSendBufferSize(); int receiveBufferSize = socket.getReceiveBufferSize(); if(receiveBufferSize != this.socketBufferSize) logger.debug("Requested socket receive buffer size was " + this.socketBufferSize + " bytes but actual size is " + receiveBufferSize + " bytes."); if(sendBufferSize != this.socketBufferSize) logger.debug("Requested socket send buffer size was " + this.socketBufferSize + " bytes but actual size is " + sendBufferSize + " bytes."); } public boolean validate(SocketDestination dest, SocketAndStreams sands) { /** * Keep track of the last time that we closed the sockets for a specific * SocketDestination. That way we know which sockets were created * *before* the SocketDestination was closed. For any sockets in the * pool at time of closure of the SocketDestination, these are shut down * immediately. For in-flight sockets that aren't in the pool at time of * closure of the SocketDestination, these are caught when they're * checked in via validate noting the relation of the timestamps. * * See bug #222. */ long lastClosedTimestamp = getLastClosedTimestamp(dest); if(sands.getCreateTimestamp() <= lastClosedTimestamp) { if(logger.isDebugEnabled()) logger.debug("Socket connection " + sands + " was created on " + new Date(sands.getCreateTimestamp() / Time.NS_PER_MS) + " before socket pool was closed and re-created (on " + new Date(lastClosedTimestamp / Time.NS_PER_MS) + ")"); return false; } Socket s = sands.getSocket(); boolean isValid = !s.isClosed() && s.isBound() && s.isConnected(); if(!isValid && logger.isDebugEnabled()) logger.debug("Socket connection " + sands + " is no longer valid, closing."); return isValid; } public int getTimeout() { return this.soTimeoutMs; } public int getNumberCreated() { return this.created.get(); } public int getNumberDestroyed() { return this.destroyed.get(); } public void close() {} /** * Returns the nanosecond-based timestamp of when this socket destination * was last closed. SocketDestination objects can be closed when their node * is marked as unavailable if the node goes down (temporarily or * otherwise). This timestamp is used to determine when sockets related to * the SocketDestination should be closed. * * <p/> * * This value starts off as 0 and is updated via setLastClosedTimestamp each * time the node is marked as unavailable. * * @return Nanosecond-based timestamp of last close */ private long getLastClosedTimestamp(SocketDestination socketDestination) { Long lastClosedTimestamp = lastClosedTimestamps.get(socketDestination); return lastClosedTimestamp != null ? lastClosedTimestamp.longValue() : 0; } /** * Assigns the last closed timestamp based on the current time in * nanoseconds. * * <p/> * * This value starts off as 0 and is updated via this method each time the * node is marked as unavailable. */ public void setLastClosedTimestamp(SocketDestination socketDestination) { lastClosedTimestamps.put(socketDestination, System.nanoTime()); } }