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