/** * Copyright 2008 ThimbleWare 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 com.thimbleware.jmemcached.protocol; import org.jboss.netty.channel.Channel; import org.jboss.netty.channel.ChannelHandler; import org.jboss.netty.channel.ChannelHandlerContext; import org.jboss.netty.channel.ChannelStateEvent; import org.jboss.netty.channel.Channels; import org.jboss.netty.channel.MessageEvent; import org.jboss.netty.channel.SimpleChannelUpstreamHandler; import org.jboss.netty.channel.group.DefaultChannelGroup; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.thimbleware.jmemcached.Cache; import com.thimbleware.jmemcached.CacheElement; import com.thimbleware.jmemcached.StatsCounter; import com.thimbleware.jmemcached.protocol.exceptions.DatabaseException; import com.thimbleware.jmemcached.protocol.exceptions.UnknownCommandException; // TODO implement flush_all delay /** * The actual command handler, which is responsible for processing the * CommandMessage instances that are inbound from the protocol decoders. * <p/> * One instance is shared among the entire pipeline, since this handler is * stateless, apart from some globals for the entire daemon. * <p/> * The command handler produces ResponseMessages which are destined for the * response encoder. */ @ChannelHandler.Sharable public final class MemcachedCommandHandler<CACHE_ELEMENT extends CacheElement> extends SimpleChannelUpstreamHandler { final Logger logger = LoggerFactory.getLogger(MemcachedCommandHandler.class); /** * The following state variables are universal for the entire daemon. These * are used for statistics gathering. In order for these values to work * properly, the handler _must_ be declared with a ChannelPipelineCoverage * of "all". */ public final String version; public final int idle_limit; public final boolean verbose; /** * The actual physical data storage. */ private final Cache<CACHE_ELEMENT> cache; /** * The channel group for the entire daemon, used for handling global cleanup * on shutdown. */ private final DefaultChannelGroup channelGroup; /** * Construct the server session handler * * @param cache * the cache to use * @param memcachedVersion * the version string to return to clients * @param verbosity * verbosity level for debugging * @param idle * how long sessions can be idle for * @param channelGroup */ public MemcachedCommandHandler(Cache cache, String memcachedVersion, boolean verbosity, int idle, DefaultChannelGroup channelGroup) { this.cache = cache; version = memcachedVersion; verbose = verbosity; idle_limit = idle; this.channelGroup = channelGroup; } /** * On open we manage some statistics, and add this connection to the channel * group. * * @param channelHandlerContext * @param channelStateEvent * @throws Exception */ @Override public void channelOpen(ChannelHandlerContext channelHandlerContext, ChannelStateEvent channelStateEvent) throws Exception { StatsCounter.total_conns.incrementAndGet(); StatsCounter.curr_conns.incrementAndGet(); channelGroup.add(channelHandlerContext.getChannel()); } /** * On close we manage some statistics, and remove this connection from the * channel group. * * @param channelHandlerContext * @param channelStateEvent * @throws Exception */ @Override public void channelClosed(ChannelHandlerContext channelHandlerContext, ChannelStateEvent channelStateEvent) throws Exception { StatsCounter.curr_conns.decrementAndGet(); channelGroup.remove(channelHandlerContext.getChannel()); } /** * The actual meat of the matter. Turn CommandMessages into executions * against the physical cache, and then pass on the downstream messages. * * @param channelHandlerContext * @param messageEvent * @throws Exception */ @Override @SuppressWarnings("unchecked") public void messageReceived(ChannelHandlerContext channelHandlerContext, MessageEvent messageEvent) throws Exception { if (!(messageEvent.getMessage() instanceof CommandMessage)) { // Ignore what this encoder can't encode. channelHandlerContext.sendUpstream(messageEvent); return; } CommandMessage<CACHE_ELEMENT> command = (CommandMessage<CACHE_ELEMENT>) messageEvent.getMessage(); Command cmd = command.cmd; int cmdKeysSize = command.keys.size(); // first process any messages in the delete queue cache.asyncEventPing(); // now do the real work if (this.verbose) { StringBuilder log = new StringBuilder(); log.append(cmd); if (command.element != null) { log.append(" ").append(command.element.getKeystring()); } for (int i = 0; i < cmdKeysSize; i++) { log.append(" ").append(command.keys.get(i)); } logger.info(log.toString()); } Channel channel = messageEvent.getChannel(); if (cmd == Command.GET || cmd == Command.GETS) { handleGets(channelHandlerContext, command, channel); } else if (cmd == Command.SET) { handleSet(channelHandlerContext, command, channel); } else if (cmd == Command.CAS) { handleCas(channelHandlerContext, command, channel); } else if (cmd == Command.ADD) { handleAdd(channelHandlerContext, command, channel); } else if (cmd == Command.REPLACE) { handleReplace(channelHandlerContext, command, channel); } else if (cmd == Command.APPEND) { handleAppend(channelHandlerContext, command, channel); } else if (cmd == Command.PREPEND) { handlePrepend(channelHandlerContext, command, channel); } else if (cmd == Command.INCR) { handleIncr(channelHandlerContext, command, channel); } else if (cmd == Command.DECR) { handleDecr(channelHandlerContext, command, channel); } else if (cmd == Command.DELETE) { handleDelete(channelHandlerContext, command, channel); } else if (cmd == Command.STATS) { handleStats(channelHandlerContext, command, cmdKeysSize, channel); } else if (cmd == Command.VERSION) { handleVersion(channelHandlerContext, command, channel); } else if (cmd == Command.QUIT) { handleQuit(channel); } else if (cmd == Command.FLUSH_ALL) { handleFlush(channelHandlerContext, command, channel); } else if (cmd == null) { // NOOP handleNoOp(channelHandlerContext, command); } else { throw new UnknownCommandException("unknown command:" + cmd); } } protected void handleNoOp(ChannelHandlerContext channelHandlerContext, CommandMessage<CACHE_ELEMENT> command) { Channels.fireMessageReceived(channelHandlerContext, new ResponseMessage(command)); } protected void handleFlush(ChannelHandlerContext channelHandlerContext, CommandMessage<CACHE_ELEMENT> command, Channel channel) throws DatabaseException, Exception { Channels.fireMessageReceived(channelHandlerContext, new ResponseMessage(command).withFlushResponse(cache .flush_all(command.time)), channel.getRemoteAddress()); } protected void handleQuit(Channel channel) { channel.disconnect(); } protected void handleVersion(ChannelHandlerContext channelHandlerContext, CommandMessage<CACHE_ELEMENT> command, Channel channel) { ResponseMessage responseMessage = new ResponseMessage(command); responseMessage.version = version; Channels.fireMessageReceived(channelHandlerContext, responseMessage, channel.getRemoteAddress()); } protected void handleStats(ChannelHandlerContext channelHandlerContext, CommandMessage<CACHE_ELEMENT> command, int cmdKeysSize, Channel channel) { String option = ""; if (cmdKeysSize > 0) { option = command.keys.get(0); } Channels.fireMessageReceived(channelHandlerContext, new ResponseMessage(command).withStatResponse(cache.stat( option, channelHandlerContext)), channel.getRemoteAddress()); } protected void handleDelete(ChannelHandlerContext channelHandlerContext, CommandMessage<CACHE_ELEMENT> command, Channel channel) throws DatabaseException, Exception { Cache.DeleteResponse dr = cache.delete(command.keys.get(0), command.time); Channels.fireMessageReceived(channelHandlerContext, new ResponseMessage(command).withDeleteResponse(dr), channel.getRemoteAddress()); } protected void handleDecr(ChannelHandlerContext channelHandlerContext, CommandMessage<CACHE_ELEMENT> command, Channel channel) throws DatabaseException, Exception { Integer incrDecrResp = cache.get_add(command.keys.get(0), -1 * command.incrAmount); Channels.fireMessageReceived(channelHandlerContext, new ResponseMessage(command) .withIncrDecrResponse(incrDecrResp), channel.getRemoteAddress()); } protected void handleIncr(ChannelHandlerContext channelHandlerContext, CommandMessage<CACHE_ELEMENT> command, Channel channel) throws DatabaseException, Exception { Integer incrDecrResp = cache.get_add(command.keys.get(0), command.incrAmount); // TODO // support // default // value // and // expiry!! Channels.fireMessageReceived(channelHandlerContext, new ResponseMessage(command) .withIncrDecrResponse(incrDecrResp), channel.getRemoteAddress()); } protected void handlePrepend(ChannelHandlerContext channelHandlerContext, CommandMessage<CACHE_ELEMENT> command, Channel channel) throws DatabaseException, Exception { Cache.StoreResponse ret; ret = cache.prepend(command.element); Channels.fireMessageReceived(channelHandlerContext, new ResponseMessage(command).withResponse(ret), channel .getRemoteAddress()); } protected void handleAppend(ChannelHandlerContext channelHandlerContext, CommandMessage<CACHE_ELEMENT> command, Channel channel) throws DatabaseException, Exception { Cache.StoreResponse ret; ret = cache.append(command.element); Channels.fireMessageReceived(channelHandlerContext, new ResponseMessage(command).withResponse(ret), channel .getRemoteAddress()); } protected void handleReplace(ChannelHandlerContext channelHandlerContext, CommandMessage<CACHE_ELEMENT> command, Channel channel) throws DatabaseException, Exception { Cache.StoreResponse ret; ret = cache.replace(command.element); Channels.fireMessageReceived(channelHandlerContext, new ResponseMessage(command).withResponse(ret), channel .getRemoteAddress()); } protected void handleAdd(ChannelHandlerContext channelHandlerContext, CommandMessage<CACHE_ELEMENT> command, Channel channel) throws DatabaseException, Exception { Cache.StoreResponse ret; ret = cache.add(command.element); Channels.fireMessageReceived(channelHandlerContext, new ResponseMessage(command).withResponse(ret), channel .getRemoteAddress()); } protected void handleCas(ChannelHandlerContext channelHandlerContext, CommandMessage<CACHE_ELEMENT> command, Channel channel) throws DatabaseException, Exception { Cache.StoreResponse ret; ret = cache.cas(command.cas_key, command.element); Channels.fireMessageReceived(channelHandlerContext, new ResponseMessage(command).withResponse(ret), channel .getRemoteAddress()); } protected void handleSet(ChannelHandlerContext channelHandlerContext, CommandMessage<CACHE_ELEMENT> command, Channel channel) throws DatabaseException, Exception { Cache.StoreResponse ret; ret = cache.set(command.element); Channels.fireMessageReceived(channelHandlerContext, new ResponseMessage(command).withResponse(ret), channel .getRemoteAddress()); } protected void handleGets(ChannelHandlerContext channelHandlerContext, CommandMessage<CACHE_ELEMENT> command, Channel channel) { CACHE_ELEMENT[] results = get(command.keys.toArray(new String[command.keys.size()])); ResponseMessage<CACHE_ELEMENT> resp = new ResponseMessage<CACHE_ELEMENT>(command).withElements(results); Channels.fireMessageReceived(channelHandlerContext, resp, channel.getRemoteAddress()); } /** * Get an element from the cache * * @param keys * the key for the element to lookup * @return the element, or 'null' in case of cache miss. */ private CACHE_ELEMENT[] get(String... keys) { return cache.get(keys); } /** * @return the current time in seconds (from epoch), used for expiries, etc. */ private static int Now() { return (int) (System.currentTimeMillis() / 1000); } }