/** *Copyright [2009-2010] [dennis zhuang(killme2008@gmail.com)] *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 net.rubyeye.xmemcached.impl; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import net.rubyeye.xmemcached.MemcachedClient; import net.rubyeye.xmemcached.MemcachedClientStateListener; import net.rubyeye.xmemcached.auth.AuthMemcachedConnectListener; import net.rubyeye.xmemcached.command.Command; import net.rubyeye.xmemcached.command.CommandType; import net.rubyeye.xmemcached.command.MapReturnValueAware; import net.rubyeye.xmemcached.command.OperationStatus; import net.rubyeye.xmemcached.command.StoreCommand; import net.rubyeye.xmemcached.command.binary.BinaryGetCommand; import net.rubyeye.xmemcached.command.binary.BinaryVersionCommand; import net.rubyeye.xmemcached.command.text.TextGetOneCommand; import net.rubyeye.xmemcached.command.text.TextVersionCommand; import net.rubyeye.xmemcached.monitor.StatisticsHandler; import net.rubyeye.xmemcached.networking.MemcachedSession; import net.rubyeye.xmemcached.networking.MemcachedSessionConnectListener; import net.rubyeye.xmemcached.transcoders.CachedData; import net.rubyeye.xmemcached.utils.InetSocketAddressWrapper; import net.rubyeye.xmemcached.utils.Protocol; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.code.yanf4j.buffer.IoBuffer; import com.google.code.yanf4j.core.Session; import com.google.code.yanf4j.core.impl.AbstractSession; import com.google.code.yanf4j.core.impl.HandlerAdapter; import com.google.code.yanf4j.util.SystemUtils; import java.util.concurrent.SynchronousQueue; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; /** * Memcached Session Handler,used for dispatching commands and session's * lifecycle management * * @author dennis * */ public class MemcachedHandler extends HandlerAdapter { private static final int MAX_HEARTBEAT_THREADS = Integer.parseInt(System .getProperty("xmemcached.heartbeat.max_threads", String.valueOf(SystemUtils.getSystemThreadCount()))); private final StatisticsHandler statisticsHandler; private ExecutorService heartBeatThreadPool; private final MemcachedSessionConnectListener listener; private final MemcachedClient client; private static final Logger log = LoggerFactory .getLogger(MemcachedHandler.class); /** * On receive message from memcached server */ @Override public final void onMessageReceived(final Session session, final Object msg) { Command command = (Command) msg; if (this.statisticsHandler.isStatistics()) { if (command.getCopiedMergeCount() > 0 && command instanceof MapReturnValueAware) { Map<String, CachedData> returnValues = ((MapReturnValueAware) command) .getReturnValues(); int size = returnValues.size(); this.statisticsHandler.statistics(CommandType.GET_HIT, size); this.statisticsHandler.statistics(CommandType.GET_MISS, command.getCopiedMergeCount() - size); } else if (command instanceof TextGetOneCommand || command instanceof BinaryGetCommand) { if (command.getResult() != null) { this.statisticsHandler.statistics(CommandType.GET_HIT); } else { this.statisticsHandler.statistics(CommandType.GET_MISS); } } else { if (command.getCopiedMergeCount() > 0) { this.statisticsHandler.statistics(command.getCommandType(), command.getCopiedMergeCount()); } else this.statisticsHandler.statistics(command.getCommandType()); } } } private volatile boolean enableHeartBeat = true; public void setEnableHeartBeat(boolean enableHeartBeat) { this.enableHeartBeat = enableHeartBeat; } public static final IoBuffer EMPTY_BUF = IoBuffer.allocate(0); /** * put command which have been sent to queue */ @Override public final void onMessageSent(Session session, Object msg) { Command command = (Command) msg; command.setStatus(OperationStatus.SENT); // After message sent,we can set the buffer to be null for gc friendly. command.setIoBuffer(EMPTY_BUF); switch (command.getCommandType()) { case ADD: case APPEND: case SET: case SET_MANY: // After message sent,we can set the value to be null for gc // friendly. if (command instanceof StoreCommand) { ((StoreCommand) command).setValue(null); } break; } } @Override public void onExceptionCaught(Session session, Throwable throwable) { log.error("XMemcached network layout exception", throwable); } /** * On session started */ @Override public void onSessionStarted(Session session) { session.setUseBlockingRead(true); session.setAttribute(HEART_BEAT_FAIL_COUNT_ATTR, new AtomicInteger(0)); for (MemcachedClientStateListener listener : this.client .getStateListeners()) { listener.onConnected(this.client, session.getRemoteSocketAddress()); } this.listener.onConnect((MemcachedTCPSession) session, this.client); } /** * Check if have to reconnect on session closed */ @Override public final void onSessionClosed(Session session) { this.client.getConnector().removeSession(session); // Clear write queue to release noreply operations. ((AbstractSession) session).clearWriteQueue(); MemcachedTCPSession memcachedSession = (MemcachedTCPSession) session; // destroy memached session memcachedSession.destroy(); if (this.client.getConnector().isStarted() && memcachedSession.isAllowReconnect()) { this.reconnect(memcachedSession); } for (MemcachedClientStateListener listener : this.client .getStateListeners()) { listener.onDisconnected(this.client, session.getRemoteSocketAddress()); } } /** * Do a heartbeat action */ @Override public void onSessionIdle(Session session) { checkHeartBeat(session); } private void checkHeartBeat(Session session) { if (this.enableHeartBeat) { log.debug( "Check session ({}) is alive,send heartbeat", session.getRemoteSocketAddress() == null ? "unknown" : SystemUtils.getRawAddress(session .getRemoteSocketAddress()) + ":" + session.getRemoteSocketAddress() .getPort()); Command versionCommand = null; CountDownLatch latch = new CountDownLatch(1); if (this.client.getProtocol() == Protocol.Binary) { versionCommand = new BinaryVersionCommand(latch, session.getRemoteSocketAddress()); } else { versionCommand = new TextVersionCommand(latch, session.getRemoteSocketAddress()); } session.write(versionCommand); // Start a check thread,avoid blocking reactor thread if (this.heartBeatThreadPool != null) { this.heartBeatThreadPool.execute(new CheckHeartResultThread( versionCommand, session)); } } } private static final String HEART_BEAT_FAIL_COUNT_ATTR = "heartBeatFailCount"; private static final int MAX_HEART_BEAT_FAIL_COUNT = Integer .parseInt(System.getProperty("xmemcached.heartbeat.max.fail.times", "3")); final static class CheckHeartResultThread implements Runnable { private final Command versionCommand; private final Session session; public CheckHeartResultThread(Command versionCommand, Session session) { super(); this.versionCommand = versionCommand; this.session = session; } public void run() { try { AtomicInteger heartBeatFailCount = (AtomicInteger) this.session .getAttribute(HEART_BEAT_FAIL_COUNT_ATTR); if (heartBeatFailCount != null) { if (!this.versionCommand.getLatch().await(2000, TimeUnit.MILLISECONDS)) { heartBeatFailCount.incrementAndGet(); } else { if (this.versionCommand.getResult() == null) { heartBeatFailCount.incrementAndGet(); } else { // reset the failure counter heartBeatFailCount.set(0); } } if (heartBeatFailCount.get() > MAX_HEART_BEAT_FAIL_COUNT) { log.warn("Session(" + SystemUtils.getRawAddress(this.session .getRemoteSocketAddress()) + ":" + this.session.getRemoteSocketAddress() .getPort() + ") heartbeat fail " + heartBeatFailCount.get() + " times,close session and try to heal it"); this.session.close();// close session heartBeatFailCount.set(0); } } } catch (InterruptedException e) { // ignore } } } /** * Auto reconect to memcached server * * @param session */ protected void reconnect(MemcachedTCPSession session) { if (!this.client.isShutdown()) { // Prevent reconnecting repeatedly synchronized (session) { if (!session.isAllowReconnect()) { return; } session.setAllowReconnect(false); } MemcachedSession memcachedTCPSession = session; InetSocketAddressWrapper inetSocketAddressWrapper = memcachedTCPSession .getInetSocketAddressWrapper(); this.client.getConnector().addToWatingQueue( new ReconnectRequest(inetSocketAddressWrapper, 0, this.client.getHealSessionInterval())); } } public void stop() { this.heartBeatThreadPool.shutdown(); } final long HEARTBEAT_PERIOD = Long.parseLong(System.getProperty( "xmemcached.heartbeat.period", "5000")); public void start() { final String name = "XMemcached-HeartBeatPool[" + client.getName() + "]"; final AtomicInteger threadCounter = new AtomicInteger(); long keepAliveTime = client.getConnector().getSessionIdleTimeout() * 3 / 2; this.heartBeatThreadPool = new ThreadPoolExecutor(1, MAX_HEARTBEAT_THREADS, keepAliveTime, TimeUnit.MILLISECONDS, new SynchronousQueue<Runnable>(), new ThreadFactory() { public Thread newThread(Runnable r) { Thread t = new Thread(r, name + "-" + threadCounter.getAndIncrement()); t.setDaemon(true); if (t.getPriority() != Thread.NORM_PRIORITY) { t.setPriority(Thread.NORM_PRIORITY); } return t; } }, new ThreadPoolExecutor.DiscardPolicy()); } public MemcachedHandler(MemcachedClient client) { super(); this.client = client; this.listener = new AuthMemcachedConnectListener(); this.statisticsHandler = new StatisticsHandler(); } }