/** *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 */ /** *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.nio.ByteBuffer; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Queue; import java.util.concurrent.CountDownLatch; import net.rubyeye.xmemcached.MemcachedOptimizer; import net.rubyeye.xmemcached.buffer.BufferAllocator; import net.rubyeye.xmemcached.command.AssocCommandAware; import net.rubyeye.xmemcached.command.Command; import net.rubyeye.xmemcached.command.CommandType; import net.rubyeye.xmemcached.command.OperationStatus; import net.rubyeye.xmemcached.command.binary.BaseBinaryCommand; import net.rubyeye.xmemcached.command.binary.BinaryGetCommand; import net.rubyeye.xmemcached.command.binary.BinaryGetMultiCommand; import net.rubyeye.xmemcached.command.binary.BinarySetMultiCommand; import net.rubyeye.xmemcached.command.binary.BinaryStoreCommand; import net.rubyeye.xmemcached.command.binary.OpCode; import net.rubyeye.xmemcached.command.text.TextGetOneCommand; import net.rubyeye.xmemcached.monitor.Constants; import net.rubyeye.xmemcached.monitor.MemcachedClientNameHolder; import net.rubyeye.xmemcached.monitor.XMemcachedMbeanServer; import net.rubyeye.xmemcached.utils.ByteUtils; import net.rubyeye.xmemcached.utils.OpaqueGenerater; 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.impl.FutureImpl; /** * Memcached command optimizer,merge single-get comands to multi-get * command,merge ByteBuffers to fit the socket's sendBufferSize etc. * * @author dennis */ public class Optimizer implements OptimizerMBean, MemcachedOptimizer { public static final int DEFAULT_MERGE_FACTOR = 50; private int mergeFactor = DEFAULT_MERGE_FACTOR; // default merge factor; private boolean optimiezeGet = true; private final boolean optimiezeSet = true; private boolean optimiezeMergeBuffer = true; private static final Logger log = LoggerFactory.getLogger(Optimizer.class); private Protocol protocol = Protocol.Binary; public Optimizer(Protocol protocol) { XMemcachedMbeanServer.getInstance().registMBean( this, this.getClass().getPackage().getName() + ":type=" + this.getClass().getSimpleName() + "-" + MemcachedClientNameHolder.getName()); this.protocol = protocol; } public void setBufferAllocator(BufferAllocator bufferAllocator) { } public int getMergeFactor() { return this.mergeFactor; } public void setMergeFactor(int mergeFactor) { if (this.mergeFactor != mergeFactor) { log.warn("change mergeFactor from " + this.mergeFactor + " to " + mergeFactor); } this.mergeFactor = mergeFactor; } public boolean isOptimizeGet() { return this.optimiezeGet; } public void setOptimizeGet(boolean optimiezeGet) { log.warn(optimiezeGet ? "Enable merge get commands" : "Disable merge get commands"); this.optimiezeGet = optimiezeGet; } public boolean isOptimizeMergeBuffer() { return this.optimiezeMergeBuffer; } public void setOptimizeMergeBuffer(boolean optimiezeMergeBuffer) { log.warn(optimiezeMergeBuffer ? "Enable merge buffers" : "Disable merge buffers"); this.optimiezeMergeBuffer = optimiezeMergeBuffer; } @SuppressWarnings("unchecked") public Command optimize(final Command currentCommand, final Queue writeQueue, final Queue<Command> executingCmds, int sendBufferSize) { Command optimiezeCommand = currentCommand; optimiezeCommand = this.optimiezeGet(writeQueue, executingCmds, optimiezeCommand); optimiezeCommand = this.optimiezeSet(writeQueue, executingCmds, optimiezeCommand, sendBufferSize); optimiezeCommand = this.optimiezeMergeBuffer(optimiezeCommand, writeQueue, executingCmds, sendBufferSize); return optimiezeCommand; } /** * merge buffers to fit socket's send buffer size * * @param currentCommand * @return * @throws InterruptedException */ @SuppressWarnings("unchecked") public final Command optimiezeMergeBuffer(Command optimiezeCommand, final Queue writeQueue, final Queue<Command> executingCmds, int sendBufferSize) { if (log.isDebugEnabled()) { log.debug("Optimieze merge buffer:" + optimiezeCommand.toString()); } if (this.optimiezeMergeBuffer && optimiezeCommand.getIoBuffer().remaining() < sendBufferSize - 24) { optimiezeCommand = this.mergeBuffer(optimiezeCommand, writeQueue, executingCmds, sendBufferSize); } return optimiezeCommand; } /** * Merge get operation to multi-get operation * * @param currentCmd * @param mergeCommands * @return * @throws InterruptedException */ @SuppressWarnings("unchecked") public final Command optimiezeGet(final Queue writeQueue, final Queue<Command> executingCmds, Command optimiezeCommand) { if (optimiezeCommand.getCommandType() == CommandType.GET_ONE || optimiezeCommand.getCommandType() == CommandType.GETS_ONE) { if (this.optimiezeGet) { optimiezeCommand = this.mergeGetCommands(optimiezeCommand, writeQueue, executingCmds, optimiezeCommand.getCommandType()); } } return optimiezeCommand; } public final Command optimiezeSet(final Queue writeQueue, final Queue<Command> executingCmds, Command optimiezeCommand, int sendBufferSize) { if (this.optimiezeSet && optimiezeCommand.getCommandType() == CommandType.SET && !optimiezeCommand.isNoreply() && this.protocol == Protocol.Binary) { optimiezeCommand = this.mergeSetCommands(optimiezeCommand, writeQueue, executingCmds, optimiezeCommand.getCommandType(), sendBufferSize); } return optimiezeCommand; } @SuppressWarnings("unchecked") private final Command mergeBuffer(final Command firstCommand, final Queue writeQueue, final Queue<Command> executingCmds, final int sendBufferSize) { Command lastCommand = firstCommand; Command nextCmd = (Command) writeQueue.peek(); if (nextCmd == null) { return lastCommand; } final List<Command> commands = this.getLocalList(); final ByteBuffer firstBuffer = firstCommand.getIoBuffer().buf(); int totalBytes = firstBuffer.remaining(); commands.add(firstCommand); boolean wasFirst = true; while (totalBytes + nextCmd.getIoBuffer().remaining() <= sendBufferSize && (nextCmd = (Command) writeQueue.peek()) != null) { if (nextCmd.getStatus() == OperationStatus.WRITING) { break; } if (nextCmd.isCancel()) { writeQueue.remove(); continue; } nextCmd.setStatus(OperationStatus.WRITING); writeQueue.remove(); if (wasFirst) { wasFirst = false; } // if it is get_one command,try to merge get commands if ((nextCmd.getCommandType() == CommandType.GET_ONE || nextCmd .getCommandType() == CommandType.GETS_ONE) && this.optimiezeGet) { nextCmd = this.mergeGetCommands(nextCmd, writeQueue, executingCmds, nextCmd.getCommandType()); } commands.add(nextCmd); lastCommand = nextCmd; totalBytes += nextCmd.getIoBuffer().remaining(); if (totalBytes > sendBufferSize) { break; } } if (commands.size() > 1) { byte[] buf = new byte[totalBytes]; int offset = 0; for (Command command : commands) { byte[] ba = command.getIoBuffer().array(); System.arraycopy(ba, 0, buf, offset, ba.length); offset += ba.length; if (command != lastCommand && (!command.isNoreply() || command instanceof BaseBinaryCommand)) { executingCmds.add(command); } } lastCommand.setIoBuffer(IoBuffer.wrap(buf)); } return lastCommand; } private final ThreadLocal<List<Command>> threadLocal = new ThreadLocal<List<Command>>() { @Override protected List<Command> initialValue() { return new ArrayList<Command>(Optimizer.this.mergeFactor); } }; public final List<Command> getLocalList() { List<Command> list = this.threadLocal.get(); list.clear(); return list; } static interface CommandCollector { public Object getResult(); public void visit(Command command); public void finish(); public CommandCollector reset(); } static class KeyStringCollector implements CommandCollector { char[] buf = new char[1024*2]; int count = 0; boolean wasFirst = true; public CommandCollector reset() { this.count = 0; this.wasFirst = true; return this; } public Object getResult() { return new String(this.buf, 0, this.count); } public void visit(Command command) { if (this.wasFirst) { this.append(command.getKey()); this.wasFirst = false; } else { this.append(" "); this.append(command.getKey()); } } private void expandCapacity(int minimumCapacity) { int newCapacity = (this.buf.length + 1) * 2; if (newCapacity < 0) { newCapacity = Integer.MAX_VALUE; } else if (minimumCapacity > newCapacity) { newCapacity = minimumCapacity; } char[] copy = new char[newCapacity]; System.arraycopy(this.buf, 0, copy, 0, Math.min(this.buf.length, newCapacity)); this.buf = copy; } private void append(String str) { int len = str.length(); if (len == 0) { return; } int newCount = this.count + len; if (newCount > this.buf.length) { this.expandCapacity(newCount); } str.getChars(0, len, this.buf, this.count); this.count = newCount; } public void finish() { // do nothing } } private static class BinarySetQCollector implements CommandCollector { ArrayList<IoBuffer> bufferList = new ArrayList<IoBuffer>(); int totalBytes; BinaryStoreCommand prevCommand; Map<Object, Command> mergeCommands; public CommandCollector reset() { this.bufferList.clear(); this.totalBytes = 0; this.prevCommand = null; this.mergeCommands = null; return this; } public Object getResult() { byte[] buf = new byte[this.totalBytes]; int offset = 0; for (IoBuffer buffer : this.bufferList) { byte[] ba = buffer.array(); System.arraycopy(ba, 0, buf, offset, ba.length); offset += ba.length; } BinarySetMultiCommand resultCommand = new BinarySetMultiCommand( null, CommandType.SET_MANY, new CountDownLatch(1)); resultCommand.setIoBuffer(IoBuffer.wrap(buf)); resultCommand.setMergeCommands(this.mergeCommands); resultCommand.setMergeCount(this.mergeCommands.size()); return resultCommand; } public void visit(Command command) { // Encode prev command if (this.prevCommand != null) { // first n-1 send setq command BinaryStoreCommand setqCmd = new BinaryStoreCommand( this.prevCommand.getKey(), this.prevCommand.getKeyBytes(), CommandType.SET, null, this.prevCommand.getExpTime(), this.prevCommand.getCas(), // set noreply to be true this.prevCommand.getValue(), true, this.prevCommand.getTranscoder()); // We must set the opaque to get error message. int opaque = OpaqueGenerater.getInstance().getNextValue(); setqCmd.setOpaque(opaque); setqCmd.encode(); this.totalBytes += setqCmd.getIoBuffer().remaining(); this.bufferList.add(setqCmd.getIoBuffer()); // GC friendly setqCmd.setIoBuffer(MemcachedHandler.EMPTY_BUF); setqCmd.setValue(null); this.prevCommand.setValue(null); this.prevCommand.setIoBuffer(MemcachedHandler.EMPTY_BUF); if (this.mergeCommands == null) { this.mergeCommands = new HashMap<Object, Command>(); } this.mergeCommands.put(opaque, this.prevCommand); } this.prevCommand = (BinaryStoreCommand) command; } public void finish() { if (this.mergeCommands == null) { return; } // prevCommand is the last command,last command must be a SET // command,ensure // previous SETQ commands sending response back BinaryStoreCommand setqCmd = new BinaryStoreCommand( this.prevCommand.getKey(), this.prevCommand.getKeyBytes(), CommandType.SET, null, this.prevCommand.getExpTime(), this.prevCommand.getCas(), // set noreply to be false. this.prevCommand.getValue(), false, this.prevCommand.getTranscoder()); // We must set the opaque to get error message. int opaque = OpaqueGenerater.getInstance().getNextValue(); setqCmd.setOpaque(opaque); setqCmd.encode(); this.bufferList.add(setqCmd.getIoBuffer()); this.totalBytes += setqCmd.getIoBuffer().remaining(); if (this.mergeCommands != null) { this.mergeCommands.put(opaque, this.prevCommand); } } } private static class BinaryGetQCollector implements CommandCollector { ArrayList<IoBuffer> bufferList = new ArrayList<IoBuffer>(50); int totalBytes; Command prevCommand; public CommandCollector reset(){ this.bufferList.clear(); this.totalBytes = 0; this.prevCommand = null; return this; } public Object getResult() { byte[] buf = new byte[this.totalBytes]; int offset = 0; for (IoBuffer buffer : this.bufferList) { byte[] ba = buffer.array(); System.arraycopy(ba, 0, buf, offset, ba.length); offset += ba.length; } BinaryGetMultiCommand resultCommand = new BinaryGetMultiCommand( null, CommandType.GET_MANY, new CountDownLatch(1)); resultCommand.setIoBuffer(IoBuffer.wrap(buf)); return resultCommand; } public void visit(Command command) { // Encode prev command if (this.prevCommand != null) { // first n-1 send getq command Command getqCommand = new BinaryGetCommand( this.prevCommand.getKey(), this.prevCommand.getKeyBytes(), null, null, OpCode.GET_KEY_QUIETLY, true); getqCommand.encode(); this.totalBytes += getqCommand.getIoBuffer().remaining(); this.bufferList.add(getqCommand.getIoBuffer()); } this.prevCommand = command; } public void finish() { // prev command is the last command,last command must be getk,ensure // getq commands sending response back Command lastGetKCommand = new BinaryGetCommand( this.prevCommand.getKey(), this.prevCommand.getKeyBytes(), CommandType.GET_ONE, new CountDownLatch(1), OpCode.GET_KEY, false); lastGetKCommand.encode(); this.bufferList.add(lastGetKCommand.getIoBuffer()); this.totalBytes += lastGetKCommand.getIoBuffer().remaining(); } } @SuppressWarnings("unchecked") private final Command mergeGetCommands(final Command currentCmd, final Queue writeQueue, final Queue<Command> executingCmds, CommandType expectedCommandType) { Map<Object, Command> mergeCommands = null; int mergeCount = 1; final CommandCollector commandCollector = this.createGetCommandCollector(); currentCmd.setStatus(OperationStatus.WRITING); commandCollector.visit(currentCmd); while (mergeCount < this.mergeFactor) { Command nextCmd = (Command) writeQueue.peek(); if (nextCmd == null) { break; } if (nextCmd.isCancel()) { writeQueue.remove(); continue; } if (nextCmd.getCommandType() == expectedCommandType) { if (mergeCommands == null) { // lazy initialize mergeCommands = new HashMap<Object, Command>( this.mergeFactor / 2); mergeCommands.put(currentCmd.getKey(), currentCmd); } if (log.isDebugEnabled()) { log.debug("Merge get command:" + nextCmd.toString()); } nextCmd.setStatus(OperationStatus.WRITING); Command removedCommand = (Command) writeQueue.remove(); // If the key is exists,add the command to associated list. if (mergeCommands.containsKey(removedCommand.getKey())) { final AssocCommandAware mergedGetCommand = (AssocCommandAware) mergeCommands .get(removedCommand.getKey()); if (mergedGetCommand.getAssocCommands() == null) { mergedGetCommand .setAssocCommands(new ArrayList<Command>(5)); } mergedGetCommand.getAssocCommands().add(removedCommand); } else { commandCollector.visit(nextCmd); mergeCommands.put(removedCommand.getKey(), removedCommand); } mergeCount++; } else { break; } } if (mergeCount == 1) { return currentCmd; } else { commandCollector.finish(); if (log.isDebugEnabled()) { log.debug("Merge optimieze:merge " + mergeCount + " get commands"); } return this.newMergedCommand(mergeCommands, mergeCount, commandCollector, expectedCommandType); } } private static final ThreadLocal<BinarySetQCollector> BIN_SET_CMD_COLLECTOR_THREAD_LOCAL = new ThreadLocal<Optimizer.BinarySetQCollector>(){ @Override protected BinarySetQCollector initialValue() { return new BinarySetQCollector(); } }; private final Command mergeSetCommands(final Command currentCmd, final Queue writeQueue, final Queue<Command> executingCmds, CommandType expectedCommandType, int sendBufferSize) { int mergeCount = 1; final CommandCollector commandCollector = BIN_SET_CMD_COLLECTOR_THREAD_LOCAL.get().reset(); currentCmd.setStatus(OperationStatus.WRITING); int totalBytes = currentCmd.getIoBuffer().remaining(); commandCollector.visit(currentCmd); while (mergeCount < this.mergeFactor && totalBytes <= sendBufferSize) { Command nextCmd = (Command) writeQueue.peek(); if (nextCmd == null) { break; } if (nextCmd.isCancel()) { writeQueue.remove(); continue; } if (nextCmd.getCommandType() == expectedCommandType && !nextCmd.isNoreply()) { if (log.isDebugEnabled()) { log.debug("Merge set command:" + nextCmd.toString()); } nextCmd.setStatus(OperationStatus.WRITING); writeQueue.remove(); commandCollector.visit(nextCmd); mergeCount++; } else { break; } totalBytes += nextCmd.getIoBuffer().remaining(); } if (mergeCount == 1) { return currentCmd; } else { commandCollector.finish(); if (log.isDebugEnabled()) { log.debug("Merge optimieze:merge " + mergeCount + " get commands"); } return (Command) commandCollector.getResult(); } } private static ThreadLocal<KeyStringCollector> TEXT_GET_CMD_COLLECTOR_THREAD_LOCAL = new ThreadLocal<Optimizer.KeyStringCollector>(){ @Override public KeyStringCollector initialValue(){ return new KeyStringCollector(); } }; private static ThreadLocal<BinaryGetQCollector> BIN_GET_CMD_COLLECTOR_THREAD_LOCAL = new ThreadLocal<Optimizer.BinaryGetQCollector>(){ @Override public BinaryGetQCollector initialValue(){ return new BinaryGetQCollector(); } }; private CommandCollector createGetCommandCollector() { switch (this.protocol) { case Binary: return BIN_GET_CMD_COLLECTOR_THREAD_LOCAL.get().reset(); default: return TEXT_GET_CMD_COLLECTOR_THREAD_LOCAL.get().reset(); } } private Command newMergedCommand(final Map<Object, Command> mergeCommands, int mergeCount, final CommandCollector commandCollector, final CommandType commandType) { if (this.protocol == Protocol.Text) { String resultKey = (String) commandCollector.getResult(); byte[] keyBytes = ByteUtils.getBytes(resultKey); byte[] cmdBytes = commandType == CommandType.GET_ONE ? Constants.GET : Constants.GETS; final byte[] buf = new byte[cmdBytes.length + 3 + keyBytes.length]; ByteUtils.setArguments(buf, 0, cmdBytes, keyBytes); TextGetOneCommand cmd = new TextGetOneCommand(resultKey, keyBytes, commandType, null); cmd.setMergeCommands(mergeCommands); cmd.setWriteFuture(new FutureImpl<Boolean>()); cmd.setMergeCount(mergeCount); cmd.setIoBuffer(IoBuffer.wrap(buf)); return cmd; } else { BinaryGetMultiCommand result = (BinaryGetMultiCommand) commandCollector .getResult(); result.setMergeCount(mergeCount); result.setMergeCommands(mergeCommands); return result; } } }