/**
* 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;
import org.apache.mina.common.IdleStatus;
import org.apache.mina.common.IoHandler;
import org.apache.mina.common.IoSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static java.lang.Integer.parseInt;
import static java.lang.String.valueOf;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.util.Iterator;
// TODO implement 'delete queue' (time on delete) and flush_all delay
/**
* The heart of the daemon, responsible for handling the creation and destruction of network
* sessions, keeping cache statistics, and (most importantly) processing inbound (parsed) commands and then passing on
* a response message for output.
*/
public final class ServerSessionHandler implements IoHandler {
final Logger logger = LoggerFactory.getLogger(ServerSessionHandler.class);
public String version;
public int curr_conns;
public int total_conns;
public int started; /* when the process was started */
public static long bytes_read;
public static long bytes_written;
public static long curr_bytes;
public int idle_limit;
public boolean verbose;
public static CharsetEncoder ENCODER = Charset.forName("US-ASCII").newEncoder();
/**
*/
protected Cache cache;
/**
* 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
*/
public ServerSessionHandler(Cache cache, String memcachedVersion, boolean verbosity, int idle) {
initStats();
this.cache = cache;
started = Now();
version = memcachedVersion;
verbose = verbosity;
idle_limit = idle;
}
/**
* Handle the creation of a new protocol session.
*
* @param session the MINA session object
*/
public void sessionCreated(IoSession session) {
int conn = total_conns++;
session.setAttribute("sess_id", valueOf(conn));
curr_conns++;
if (this.verbose) {
logger.info(session.getAttribute("sess_id") + " CONNECTED");
}
}
/**
* Handle the opening of a new session.
*
* @param session the MINA session object
*/
public void sessionOpened(IoSession session) {
if (this.idle_limit > 0) {
session.setIdleTime(IdleStatus.BOTH_IDLE, this.idle_limit);
}
session.setAttribute("waiting_for", 0);
}
/**
* Handle the closing of a session.
*
* @param session the MINA session object
*/
public void sessionClosed(IoSession session) {
curr_conns--;
if (this.verbose) {
logger.info(session.getAttribute("sess_id") + " DIS-CONNECTED");
}
}
/**
* Handle the reception of an inbound command, which has already been pre-processed by the CommandDecoder.
*
* @param session the MINA session
* @param message the message itself
* @throws CharacterCodingException
*/
public void messageReceived(IoSession session, Object message) throws CharacterCodingException {
CommandMessage command = (CommandMessage) message;
String cmd = command.cmd;
int cmdKeysSize = command.keys.size();
// first process any messages in the delete queue
cache.processDeleteQueue();
// now do the real work
if (this.verbose) {
StringBuffer log = new StringBuffer();
log.append(session.getAttribute("sess_id")).append(" ");
log.append(cmd);
if (command.element != null) {
log.append(" ").append(command.element.keystring);
}
for (int i = 0; i < cmdKeysSize; i++) {
log.append(" ").append(command.keys.get(i));
}
logger.info(log.toString());
}
ResponseMessage r = new ResponseMessage();
if (cmd == Commands.GET || cmd == Commands.GETS) {
for (int i = 0; i < cmdKeysSize; i++) {
MCElement result = get(command.keys.get(i));
if (result != null) {
r.out.putString("VALUE " + result.keystring + " " + result.flags + " " + result.data_length + (cmd == Commands.GETS ? " " + result.cas_unique : "") + "\r\n", ENCODER);
r.out.put(result.data, 0, result.data_length);
r.out.putString("\r\n", ENCODER);
}
}
r.out.putString("END\r\n", ENCODER);
} else if (cmd == Commands.SET) {
String ret = set(command.element);
if (!command.noreply)
r.out.putString(ret, ENCODER);
} else if (cmd == Commands.CAS) {
String ret = cas(command.cas_key, command.element);
if (!command.noreply)
r.out.putString(ret, ENCODER);
} else if (cmd == Commands.ADD) {
String ret = add(command.element);
if (!command.noreply)
r.out.putString(ret, ENCODER);
} else if (cmd == Commands.REPLACE) {
String ret = replace(command.element);
if (!command.noreply)
r.out.putString(ret, ENCODER);
} else if (cmd == Commands.APPEND) {
String ret = append(command.element);
if (!command.noreply)
r.out.putString(ret, ENCODER);
} else if (cmd == Commands.PREPEND) {
String ret = prepend(command.element);
if (!command.noreply)
r.out.putString(ret, ENCODER);
} else if (cmd == Commands.INCR) {
String ret = get_add(command.keys.get(0), parseInt(command.keys.get(1)));
if (!command.noreply)
r.out.putString(ret, ENCODER);
} else if (cmd == Commands.DECR) {
String ret = get_add(command.keys.get(0), -1 * parseInt(command.keys.get(1)));
if (!command.noreply)
r.out.putString(ret, ENCODER);
} else if (cmd == Commands.DELETE) {
String ret = delete(command.keys.get(0), command.time);
if (!command.noreply)
r.out.putString(ret, ENCODER);
} else if (cmd == Commands.STATS) {
String option = "";
if (cmdKeysSize > 0) {
option = command.keys.get(0);
}
r.out.putString(stat(option), ENCODER);
} else if (cmd == Commands.VERSION) {
r.out.putString("VERSION ", ENCODER);
r.out.putString(version, ENCODER);
r.out.putString("\r\n", ENCODER);
} else if (cmd == Commands.QUIT) {
session.close();
} else if (cmd == Commands.FLUSH_ALL) {
String ret = flush_all(command.time);
if (!command.noreply)
r.out.putString(ret, ENCODER);
} else {
r.out.putString("ERROR\r\n", ENCODER);
logger.error("error; unrecognized command: " + cmd);
}
session.write(r);
}
/**
* Called on message delivery.
*
* @param session the MINA session
* @param message the message sent
*/
public void messageSent(IoSession session, Object message) {
if (this.verbose) {
logger.info(session.getAttribute("sess_id") + " SENT");
}
}
/**
* Triggered when a session has gone idle.
*
* @param session the MINA session
* @param status the idle status
*/
public void sessionIdle(IoSession session, IdleStatus status) {
// disconnect an idle client
session.close();
}
/**
* Triggered when an exception is caught by the protocol handler
*
* @param session the MINA session
* @param cause the exception
*/
public void exceptionCaught(IoSession session, Throwable cause) throws CharacterCodingException {
// close the connection on exceptional situation
logger.error(session.getAttribute("sess_id") + " EXCEPTION", cause);
ResponseMessage r = new ResponseMessage();
// this needs to make a better distinction between server and client error messages
r.out.putString("CLIENT_ERROR\r\n", ENCODER);
session.write(r);
}
/**
* Handle the deletion of an item from the cache.
*
* @param key the key for the item
* @param time only delete the element if time (time in seconds)
* @return the message response
*/
protected String delete(String key, int time) {
return getDeleteResponseString(cache.delete(key, time));
}
private String getDeleteResponseString(Cache.DeleteResponse deleteResponse) {
if (deleteResponse == Cache.DeleteResponse.DELETED) return "DELETED\r\n";
else return "NOT_FOUND\r\n";
}
/**
* Add an element to the cache
*
* @param e the element to add
* @return the message response string
*/
protected String add(MCElement e) {
return getStoreResponseString(cache.add(e));
}
/**
* Find the string response message which is equivalent to a response to a set/add/replace message
* in the cache
*
* @param storeResponse the response code
* @return the string to output on the network
*/
protected String getStoreResponseString(Cache.StoreResponse storeResponse) {
switch (storeResponse) {
case EXISTS:
return "EXISTS\r\n";
case NOT_FOUND:
return "NOT_FOUND\r\n";
case NOT_STORED:
return "NOT_STORED\r\n";
case STORED:
return "STORED\r\n";
}
throw new RuntimeException("unknown store response from cache: " + storeResponse);
}
/**
* Replace an element in the cache
*
* @param e the element to replace
* @return the message response string
*/
protected String replace(MCElement e) {
return getStoreResponseString(cache.replace(e));
}
/**
* Append bytes to an element in the cache
* @param element the element to append to
* @return the message response string
*/
protected String append(MCElement element) {
return getStoreResponseString(cache.append(element));
}
/**
* Prepend bytes to an element in the cache
* @param element the element to append to
* @return the message response string
*/
protected String prepend(MCElement element) {
return getStoreResponseString(cache.prepend(element));
}
/**
* Set an element in the cache
*
* @param e the element to set
* @return the message response string
*/
protected String set(MCElement e) {
return getStoreResponseString(cache.set(e));
}
/**
* Check and set an element in the cache
*
* @param cas_key the unique cas id for the element, to match against
* @param e the element to set @return the message response string
* @return the message response string
*/
protected String cas(Long cas_key, MCElement e) {
return getStoreResponseString(cache.cas(cas_key, e));
}
/**
* Increment an (integer) element in the cache
*
* @param key the key to increment
* @param mod the amount to add to the value
* @return the message response
*/
protected String get_add(String key, int mod) {
Integer ret = cache.get_add(key, mod);
if (ret == null)
return "NOT_FOUND\r\n";
else
return valueOf(ret) + "\r\n";
}
/**
* Check whether an element is in the cache and non-expired
*
* @param key the key for the element to lookup
* @return whether the element is in the cache and is live
*/
protected boolean is_there(String key) {
return cache.isThere(key);
}
/**
* Get an element from the cache
*
* @param key the key for the element to lookup
* @return the element, or 'null' in case of cache miss.
*/
protected MCElement get(String key) {
return cache.get(key);
}
/**
* @return the current time in seconds (from epoch), used for expiries, etc.
*/
protected final int Now() {
return (int) (System.currentTimeMillis() / 1000);
}
/**
* Initialize all statistic counters
*/
protected void initStats() {
curr_bytes = 0;
curr_conns = 0;
total_conns = 0;
bytes_read = 0;
bytes_written = 0;
}
/**
* Return runtime statistics
*
* @param arg additional arguments to the stats command
* @return the full command response
*/
protected String stat(String arg) {
StringBuilder builder = new StringBuilder();
if (arg.equals("keys")) {
Iterator itr = this.cache.keys().iterator();
while (itr.hasNext()) {
builder.append("STAT key ").append(itr.next()).append("\r\n");
}
builder.append("END\r\n");
return builder.toString();
}
// stats we know
builder.append("STAT version ").append(version).append("\r\n");
builder.append("STAT cmd_gets ").append(valueOf(cache.getGetCmds())).append("\r\n");
builder.append("STAT cmd_sets ").append(valueOf(cache.getSetCmds())).append("\r\n");
builder.append("STAT get_hits ").append(valueOf(cache.getGetHits())).append("\r\n");
builder.append("STAT get_misses ").append(valueOf(cache.getGetMisses())).append("\r\n");
builder.append("STAT curr_connections ").append(valueOf(curr_conns)).append("\r\n");
builder.append("STAT total_connections ").append(valueOf(total_conns)).append("\r\n");
builder.append("STAT time ").append(valueOf(Now())).append("\r\n");
builder.append("STAT uptime ").append(valueOf(Now() - this.started)).append("\r\n");
builder.append("STAT cur_items ").append(valueOf(this.cache.getCurrentItems())).append("\r\n");
builder.append("STAT limit_maxbytes ").append(valueOf(this.cache.getLimitMaxBytes())).append("\r\n");
builder.append("STAT current_bytes ").append(valueOf(this.cache.getCurrentBytes())).append("\r\n");
builder.append("STAT free_bytes ").append(valueOf(Runtime.getRuntime().freeMemory())).append("\r\n");
// stuff we know nothing about
builder.append("STAT pid 0\r\n");
builder.append("STAT rusage_user 0:0\r\n");
builder.append("STAT rusage_system 0:0\r\n");
builder.append("STAT connection_structures 0\r\n");
builder.append("STAT bytes_read 0\r\n");
builder.append("STAT bytes_written 0\r\n");
builder.append("END\r\n");
return builder.toString();
}
/**
* Flush all cache entries
*
* @return command response
*/
protected boolean flush_all() {
return cache.flush_all();
}
/**
* Flush all cache entries with a timestamp after a given expiration time
*
* @param expire the flush time in seconds
* @return command response
*/
protected String flush_all(int expire) {
return cache.flush_all(expire) ? "OK\r\n" : "ERROR\r\n";
}
}