org.apache.geode.redis.internal.ByteToCommandDecoder.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.geode.redis.internal.ByteToCommandDecoder.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
 * agreements. See the NOTICE file distributed with this work for additional information regarding
 * copyright ownership. The ASF licenses this file to You 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 org.apache.geode.redis.internal;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

import java.util.ArrayList;
import java.util.List;

/**
 * This is the first part of the channel pipeline for Netty. Here incoming bytes are read and a
 * created {@link Command} is sent down the pipeline. It is unfortunate that this class is not
 * {@link io.netty.channel.ChannelHandler.Sharable} because no state is kept in this class. State is
 * kept by {@link ByteToMessageDecoder}, it may be worthwhile to look at a different decoder setup
 * as to avoid allocating a decoder for every new connection.
 * <p>
 * The code flow of the protocol parsing may not be exactly Java like, but this is done very
 * intentionally. It was found that in cases where large Redis requests are sent that end up being
 * fragmented, throwing exceptions when the command could not be fully parsed took up an enormous
 * amount of cpu time. The simplicity of the Redis protocol allows us to just back out and wait for
 * more data, while exceptions are left to malformed requests which should never happen if using a
 * proper Redis client.
 * 
 *
 */
public class ByteToCommandDecoder extends ByteToMessageDecoder {

    /**
     * Important note
     * 
     * Do not use '' <-- java primitive chars. Redis uses {@link Coder#CHARSET} encoding so we should
     * not risk java handling char to byte conversions, rather just hard code {@link Coder#CHARSET}
     * chars as bytes
     */

    private static final byte rID = 13; // '\r';
    private static final byte nID = 10; // '\n';
    private static final byte bulkStringID = 36; // '$';
    private static final byte arrayID = 42; // '*';
    private static final int MAX_BULK_STRING_LENGTH = 512 * 1024 * 1024; // 512 MB

    public ByteToCommandDecoder() {
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        Command c = null;
        do {
            in.markReaderIndex();
            c = parse(in);
            if (c == null) {
                in.resetReaderIndex();
                return;
            }
            out.add(c);
        } while (in.isReadable()); // Try to take advantage of pipelining if it is being used
    }

    private Command parse(ByteBuf buffer) throws RedisCommandParserException {
        if (buffer == null)
            throw new NullPointerException();
        if (!buffer.isReadable())
            return null;

        byte firstB = buffer.readByte();
        if (firstB != arrayID)
            throw new RedisCommandParserException("Expected: " + (char) arrayID + " Actual: " + (char) firstB);
        ArrayList<byte[]> commandElems = new ArrayList<byte[]>();

        if (!parseArray(commandElems, buffer))
            return null;

        return new Command(commandElems);
    }

    private boolean parseArray(ArrayList<byte[]> commandElems, ByteBuf buffer) throws RedisCommandParserException {
        byte currentChar;
        int arrayLength = parseCurrentNumber(buffer);
        if (arrayLength == Integer.MIN_VALUE || !parseRN(buffer))
            return false;
        if (arrayLength < 0 || arrayLength > 1000000000)
            throw new RedisCommandParserException("invalid multibulk length");

        for (int i = 0; i < arrayLength; i++) {
            if (!buffer.isReadable())
                return false;
            currentChar = buffer.readByte();
            if (currentChar == bulkStringID) {
                byte[] newBulkString = parseBulkString(buffer);
                if (newBulkString == null)
                    return false;
                commandElems.add(newBulkString);
            } else
                throw new RedisCommandParserException("expected: \'$\', got \'" + (char) currentChar + "\'");
        }
        return true;
    }

    /**
     * Helper method to parse a bulk string when one is seen
     * 
     * @param buffer Buffer to read from
     * @return byte[] representation of the Bulk String read
     * @throws RedisCommandParserException Thrown when there is illegal syntax
     */
    private byte[] parseBulkString(ByteBuf buffer) throws RedisCommandParserException {
        int bulkStringLength = parseCurrentNumber(buffer);
        if (bulkStringLength == Integer.MIN_VALUE)
            return null;
        if (bulkStringLength > MAX_BULK_STRING_LENGTH)
            throw new RedisCommandParserException(
                    "invalid bulk length, cannot exceed max length of " + MAX_BULK_STRING_LENGTH);
        if (!parseRN(buffer))
            return null;

        if (!buffer.isReadable(bulkStringLength))
            return null;
        byte[] bulkString = new byte[bulkStringLength];
        buffer.readBytes(bulkString);

        if (!parseRN(buffer))
            return null;

        return bulkString;
    }

    /**
     * Helper method to parse the number at the beginning of the buffer
     * 
     * @param buffer Buffer to read
     * @return The number found at the beginning of the buffer
     */
    private int parseCurrentNumber(ByteBuf buffer) {
        int number = 0;
        int readerIndex = buffer.readerIndex();
        byte b = 0;
        while (true) {
            if (!buffer.isReadable())
                return Integer.MIN_VALUE;
            b = buffer.readByte();
            if (Character.isDigit(b)) {
                number = number * 10 + (int) (b - '0');
                readerIndex++;
            } else {
                buffer.readerIndex(readerIndex);
                break;
            }
        }
        return number;
    }

    /**
     * Helper method that is called when the next characters are supposed to be "\r\n"
     * 
     * @param buffer Buffer to read from
     * @throws RedisCommandParserException Thrown when the next two characters are not "\r\n"
     */
    private boolean parseRN(ByteBuf buffer) throws RedisCommandParserException {
        if (!buffer.isReadable(2))
            return false;
        byte b = buffer.readByte();
        if (b != rID)
            throw new RedisCommandParserException("expected \'" + (char) rID + "\', got \'" + (char) b + "\'");
        b = buffer.readByte();
        if (b != nID)
            throw new RedisCommandParserException("expected: \'" + (char) nID + "\', got \'" + (char) b + "\'");
        return true;
    }

}