package org.infinispan.server.hotrod;
import java.net.InetSocketAddress;
import java.util.List;
import java.util.function.Predicate;
import org.infinispan.commons.logging.LogFactory;
import org.infinispan.commons.util.Util;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.server.core.logging.Log;
import org.infinispan.server.core.transport.ExtendedByteBufJava;
import org.infinispan.server.core.transport.NettyTransport;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufUtil;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
/**
* Decoder that will decode hotrod messages and then send a {@link CacheDecodeContext} down the pipeline.
*
* @author wburns
* @since 9.0
*/
public class HotRodDecoder extends ByteToMessageDecoder {
private final static Log log = LogFactory.getLog(HotRodDecoder.class, Log.class);
private final EmbeddedCacheManager cacheManager;
private final NettyTransport transport;
private final Predicate<? super String> ignoreCache;
private final HotRodServer server;
CacheDecodeContext decodeCtx;
Throwable previousException;
private HotRodDecoderState state = HotRodDecoderState.DECODE_HEADER;
private boolean resetRequested = true;
public HotRodDecoder(EmbeddedCacheManager cacheManager, NettyTransport transport, HotRodServer server,
Predicate<? super String> ignoreCache) {
this.cacheManager = cacheManager;
this.transport = transport;
this.ignoreCache = ignoreCache;
this.server = server;
this.decodeCtx = new CacheDecodeContext(server);
}
public NettyTransport getTransport() {
return transport;
}
void resetNow() {
decodeCtx = new CacheDecodeContext(server);
decodeCtx.header = new HotRodHeader();
state = HotRodDecoderState.DECODE_HEADER;
resetRequested = false;
}
/**
* Should be called when state is transferred. This also marks the buffer read position so when we can't read bytes
* it will be reset to here.
*
* @param newState new hotrod decoder state
* @param buf the byte buffer to mark
*/
protected void state(HotRodDecoderState newState, ByteBuf buf) {
buf.markReaderIndex();
state = newState;
}
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
try {
if (CacheDecodeContext.isTrace) {
log.tracef("Decode buffer %s using instance @%x", dumpHexByteBuf(in), System.identityHashCode(this));
}
if (resetRequested) {
if (CacheDecodeContext.isTrace)
log.tracef("Reset cached decoder data: %s", decodeCtx);
resetNow();
}
// Mark the index to the beginning, just in case
in.markReaderIndex();
switch (state) {
// These are all fall through cases which means they call to the one below if they needed additional
// processing
case DECODE_HEADER:
if (!decodeHeader(((InetSocketAddress) ctx.channel().remoteAddress()).getAddress().isLoopbackAddress(),
in, out)) {
break;
}
state(HotRodDecoderState.DECODE_KEY, in);
case DECODE_KEY:
if (!decodeKey(in, out)) {
break;
}
state(HotRodDecoderState.DECODE_PARAMETERS, in);
case DECODE_PARAMETERS:
if (!decodeParameters(in, out)) {
break;
}
state(HotRodDecoderState.DECODE_VALUE, in);
case DECODE_VALUE:
if (!decodeValue(in, out)) {
break;
}
state(HotRodDecoderState.DECODE_HEADER, in);
break;
// These are terminal cases, meaning they only perform this operation and break
case DECODE_HEADER_CUSTOM:
readCustomHeader(in, out);
break;
case DECODE_KEY_CUSTOM:
readCustomKey(in, out);
break;
case DECODE_VALUE_CUSTOM:
readCustomValue(in, out);
break;
}
} catch (Throwable t) {
previousException = t;
resetRequested = true;
// Faster than throwing exception
ctx.pipeline().fireExceptionCaught(new HotRodException(decodeCtx.createExceptionResponse(t), t.getMessage(), t));
}
}
private static String dumpHexByteBuf(ByteBuf in) {
int maxLength = 32;
StringBuilder sb = new StringBuilder(maxLength * 2 + 20);
sb.append('(').append(in.readableBytes()).append(')');
int startIndex;
if (in.readableBytes() < maxLength) {
startIndex = in.readerIndex();
} else {
startIndex = in.writerIndex() - maxLength;
sb.append("...");
}
for (int i = startIndex; i < in.writerIndex(); i++) {
Util.addHexByte(sb, in.getByte(i));
}
return sb.toString();
}
boolean decodeHeader(boolean isLoopBack, ByteBuf in, List<Object> out) throws Exception {
boolean shouldContinue = readHeader(in);
// If there was nothing present it means we throw this decoding away and start fresh
if (!shouldContinue) {
return false;
}
HotRodHeader header = decodeCtx.header;
// Check if this cache can be accessed or not
if (ignoreCache.test(header.cacheName)) {
throw new CacheUnavailableException();
}
decodeCtx.obtainCache(cacheManager, isLoopBack);
HotRodOperation op = header.op;
switch (op.getDecoderRequirements()) {
case HEADER_CUSTOM:
state(HotRodDecoderState.DECODE_HEADER_CUSTOM, in);
readCustomHeader(in, out);
return false;
case HEADER:
// If all we needed was header, we have everything already!
out.add(decodeCtx);
resetRequested = true;
return false;
default:
// Continue to key
return true;
}
}
/**
* Reads the header and returns whether we should try to continue
*
* @param buffer the buffer to read the header from
* @return whether or not we should continue
* @throws Exception
*/
boolean readHeader(ByteBuf buffer) throws Exception {
VersionedDecoder decoder = decodeCtx.decoder;
HotRodHeader header = decodeCtx.header;
if (decoder == null) {
if (buffer.readableBytes() < 1) {
buffer.resetReaderIndex();
return false;
}
short magic = buffer.readUnsignedByte();
if (CacheDecodeContext.isTrace)
log.tracef("Header magic: %d", magic);
if (magic != Constants.MAGIC_REQ) {
if (previousException == null) {
dumpBuffer("Invalid magic id", buffer);
throw new InvalidMagicIdException("Error reading magic byte or message id: " + magic);
} else {
if (CacheDecodeContext.isTrace) {
log.tracef("Error happened previously, ignoring %d byte until we find the magic number again", magic);
}
return false;
}
} else {
previousException = null;
}
long messageId = ExtendedByteBufJava.readMaybeVLong(buffer);
if (messageId == Long.MIN_VALUE) {
return false;
}
if (CacheDecodeContext.isTrace) {
log.tracef("Header message id: %d", messageId);
}
header.messageId = messageId;
if (buffer.readableBytes() < 1) {
buffer.resetReaderIndex();
return false;
}
byte version = (byte) buffer.readUnsignedByte();
header.version = version;
if (CacheDecodeContext.isTrace) {
log.tracef("Header version: %d", version);
}
if (Constants.isVersion2x(version)) {
decoder = new Decoder2x();
} else if (Constants.isVersion1x(version)) {
decoder = new Decoder10();
} else {
throw new UnknownVersionException("Unknown version:" + version, version, messageId);
}
decodeCtx.decoder = decoder;
// This way we won't have to reread the decoder related material again
buffer.markReaderIndex();
}
try {
if (!decoder.readHeader(buffer, header.version, header.messageId, header)) {
return false;
}
if (CacheDecodeContext.isTrace) {
log.tracef("Decoded header %s", header);
}
return true;
} catch (HotRodUnknownOperationException | SecurityException e) {
throw e;
} catch (Exception e) {
throw new RequestParsingException("Unable to parse header", header.version, header.messageId, e);
}
}
private static void dumpBuffer(String prefix, ByteBuf in) {
if (log.isTraceEnabled()) {
String dump = ByteBufUtil.hexDump(in).toUpperCase();
log.tracef("%s error encountered, the buffer contains: %s", prefix, dump);
}
}
private void readCustomHeader(ByteBuf in, List<Object> out) {
decodeCtx.decoder.customReadHeader(decodeCtx.header, in, decodeCtx, out);
// If out was written to, it means we read everything, else we have to reread again
if (!out.isEmpty()) {
resetRequested = true;
}
}
boolean decodeKey(ByteBuf in, List<Object> out) {
HotRodOperation op = decodeCtx.header.op;
// If we want a single key read that - else we do try for custom read
if (op.requiresKey()) {
byte[] bytes = ExtendedByteBufJava.readMaybeRangedBytes(in);
// If the bytes don't exist then we need to reread
if (bytes != null) {
if (CacheDecodeContext.isTrace) {
log.tracef("Body key: %s", Util.toHexString(bytes));
}
decodeCtx.key = bytes;
} else {
return false;
}
}
switch (op.getDecoderRequirements()) {
case KEY_CUSTOM:
state(HotRodDecoderState.DECODE_KEY_CUSTOM, in);
readCustomKey(in, out);
return false;
case KEY:
out.add(decodeCtx);
resetRequested = true;
return false;
default:
return true;
}
}
private void readCustomKey(ByteBuf in, List<Object> out) {
decodeCtx.decoder.customReadKey(decodeCtx.header, in, decodeCtx, out);
// If out was written to, it means we read everything, else we have to reread again
if (!out.isEmpty()) {
resetRequested = true;
}
}
boolean decodeParameters(ByteBuf in, List<Object> out) {
CacheDecodeContext.RequestParameters params = decodeCtx.decoder.readParameters(decodeCtx.header, in);
if (params != null) {
if (CacheDecodeContext.isTrace) {
log.tracef("Body parameters: %s", params);
}
decodeCtx.params = params;
if (decodeCtx.header.op.getDecoderRequirements() == DecoderRequirements.PARAMETERS) {
out.add(decodeCtx);
resetRequested = true;
return false;
}
return true;
} else {
return false;
}
}
boolean decodeValue(ByteBuf in, List<Object> out) {
HotRodOperation op = decodeCtx.header.op;
if (op.requireValue()) {
int valueLength = decodeCtx.params.valueLength;
if (in.readableBytes() < valueLength) {
return false;
}
byte[] bytes = new byte[valueLength];
in.readBytes(bytes);
decodeCtx.operationDecodeContext = bytes;
if (CacheDecodeContext.isTrace) {
log.tracef("Body value: %s", Util.toHexString(bytes));
}
}
switch (op.getDecoderRequirements()) {
case VALUE_CUSTOM:
state(HotRodDecoderState.DECODE_VALUE_CUSTOM, in);
readCustomValue(in, out);
return false;
case VALUE:
out.add(decodeCtx);
resetRequested = true;
return false;
}
return true;
}
private void readCustomValue(ByteBuf in, List<Object> out) {
decodeCtx.decoder.customReadValue(decodeCtx.header, in, decodeCtx, out);
// If out was written to, it means we read everything, else we have to reread again
if (!out.isEmpty()) {
resetRequested = true;
}
}
}