All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.permazen.kv.raft.msg.Message Maven / Gradle / Ivy


/*
 * Copyright (C) 2015 Archie L. Cobbs. All rights reserved.
 */

package io.permazen.kv.raft.msg;

import com.google.common.base.Preconditions;

import io.permazen.kv.raft.Timestamp;
import io.permazen.util.ByteUtil;
import io.permazen.util.LongEncoder;
import io.permazen.util.UnsignedIntEncoder;

import java.io.StringWriter;
import java.nio.ByteBuffer;

/**
 * Support superclass for Raft messages.
 */
public abstract class Message {

    // Codes identifying serialized message type
    static final byte APPEND_REQUEST_TYPE = 1;
    static final byte APPEND_RESPONSE_TYPE = 2;
    static final byte COMMIT_REQUEST_TYPE = 3;
    static final byte COMMIT_RESPONSE_TYPE = 4;
    static final byte GRANT_VOTE_TYPE = 5;
    static final byte INSTALL_SNAPSHOT_TYPE = 6;
    static final byte REQUEST_VOTE_TYPE = 7;
    static final byte PING_REQUEST_TYPE = 8;
    static final byte PING_RESPONSE_TYPE = 9;
    static final byte MAX_TYPE = 10;

    // Serialization version number
    static final int VERSION_1 = 1;
    static final int VERSION_2 = 2;
    static final int VERSION_3 = 3;

    // Minimum buffer size to use a direct buffer
    private static final int MIN_DIRECT_BUFFER_SIZE = 128;

    private final byte type;
    private final int clusterId;
    private final String senderId;
    private final String recipientId;
    private final long term;

    protected Message(byte type, int clusterId, String senderId, String recipientId, long term) {
        this.type = type;
        this.clusterId = clusterId;
        this.senderId = senderId;
        this.recipientId = recipientId;
        this.term = term;
    }

    protected Message(byte type, ByteBuffer buf, int version) {
        this.type = type;
        this.clusterId = buf.getInt();
        this.senderId = Message.getString(buf);
        this.recipientId = Message.getString(buf);
        this.term = LongEncoder.read(buf);
    }

    void checkArguments() {
        Preconditions.checkArgument(this.type > 0 && this.type < MAX_TYPE);
        Preconditions.checkArgument(this.clusterId != 0);
        Preconditions.checkArgument(this.senderId != null);
        Preconditions.checkArgument(this.recipientId != null);
        Preconditions.checkArgument(this.term > 0);
    }

// Properties

    /**
     * Get the cluster ID of the sender.
     *
     * @return sender's cluster ID
     */
    public int getClusterId() {
        return this.clusterId;
    }

    /**
     * Get the identity of the sender.
     *
     * @return sender's unique identity string
     */
    public String getSenderId() {
        return this.senderId;
    }

    /**
     * Get the identity of the recipient.
     *
     * @return recipient's unique identity string
     */
    public String getRecipientId() {
        return this.recipientId;
    }

    /**
     * Get the term of the sender of this message.
     *
     * @return requester's unique ID
     */
    public long getTerm() {
        return this.term;
    }

    /**
     * Determine whether this message is only sent by leaders.
     *
     * @return true if receipt of this message implies sender is a leader
     */
    public boolean isLeaderMessage() {
        return false;
    }

// MessageSwitch

    /**
     * Apply the visitor pattern based on this instance's type.
     *
     * @param handler target for visit
     */
    public abstract void visit(MessageSwitch handler);

// (De)serialization methods

    // Check encoding format version
    static void checkVersion(int version) {
        switch (version) {
        case Message.VERSION_1:
        case Message.VERSION_2:
        case Message.VERSION_3:
            break;
        default:
            throw new IllegalArgumentException("unrecognized message format version " + version);
        }
    }

    /**
     * Get the current (i.e., maximum known) protocol version number.
     *
     * @return latest message protocol version
     */
    public static int getCurrentProtocolVersion() {
        return Message.VERSION_3;
    }

    /**
     * Read the protocol version number header from the message encoded in the given input.
     *
     * @param buf source for encoded message
     * @return decoded message protocol version
     */
    public static int decodeProtocolVersion(ByteBuffer buf) {
        return buf.get() & 0xff;
    }

    /**
     * Decode a message from the given input. The protocol version number header should already have been read.
     *
     * 

* Note that data is not necessarily copied out of {@code buf}, so the returned instance may become invalid * if the data in {@code buf} gets overwritten. * * @param buf source for encoded message * @param version message version * @return decoded message * @throws java.nio.BufferUnderflowException if there is not enough data * @throws IllegalArgumentException if {@code version} is bogus * @throws IllegalArgumentException if encoded message is bogus * @throws IllegalArgumentException if there is trailing garbage */ public static Message decode(ByteBuffer buf, int version) { // Check encoding format version Message.checkVersion(version); // Read type and decode message final Message msg; final byte type = buf.get(); switch (type) { case APPEND_REQUEST_TYPE: msg = new AppendRequest(buf, version); break; case APPEND_RESPONSE_TYPE: msg = new AppendResponse(buf, version); break; case COMMIT_REQUEST_TYPE: msg = new CommitRequest(buf, version); break; case COMMIT_RESPONSE_TYPE: msg = new CommitResponse(buf, version); break; case GRANT_VOTE_TYPE: msg = new GrantVote(buf, version); break; case INSTALL_SNAPSHOT_TYPE: msg = new InstallSnapshot(buf, version); break; case REQUEST_VOTE_TYPE: msg = new RequestVote(buf, version); break; case PING_REQUEST_TYPE: msg = new PingRequest(buf, version); break; case PING_RESPONSE_TYPE: msg = new PingResponse(buf, version); break; default: throw new IllegalArgumentException("invalid message type " + type); } // Check for trailing garbage if (buf.hasRemaining()) throw new IllegalArgumentException("buffer contains " + buf.remaining() + " bytes of extra garbage after " + msg); // Done return msg; } /** * Serialize this instance. * * @param version protocol encoding version number * @throws IllegalArgumentException if {@code version} is bogus * @return encoded message */ public ByteBuffer encode(int version) { // Check encoding format version Message.checkVersion(version); // Allocate buffer final int size = this.calculateSize(version); final ByteBuffer buf = size >= MIN_DIRECT_BUFFER_SIZE ? ByteBuffer.allocateDirect(size) : ByteBuffer.allocate(size); // Encode message this.writeTo(buf, version); if (buf.hasRemaining()) throw new RuntimeException("internal error: " + buf.remaining() + " remaining bytes in buffer from " + this); return (ByteBuffer)buf.flip(); } /** * Serialize this instance into the given buffer. * * @param buf destination for encoded data * @param version protocol encoding version number * @throws java.nio.BufferOverflowException if data overflows {@code buf} * @throws IllegalArgumentException if {@code version} is bogus */ public void writeTo(ByteBuffer buf, int version) { Message.checkVersion(version); buf.put((byte)version); buf.put(this.type); buf.putInt(this.clusterId); Message.putString(buf, this.senderId); Message.putString(buf, this.recipientId); LongEncoder.write(buf, this.term); } /** * Calculate an upper bound on the number of bytes required by {@link #writeTo writeTo()}. * * @param version protocol encoding version * @return an upper bound on the number of encoded bytes */ protected int calculateSize(int version) { Message.checkVersion(version); return 1 // protocol encoding version + 1 // message type + 4 // cluster ID + Message.calculateSize(this.senderId) + Message.calculateSize(this.recipientId) + LongEncoder.encodeLength(this.term); } // Object @Override public abstract String toString(); // Helpers /** * Serialize a {@link ByteBuffer} into the buffer. * * @param dest destination for encoded data * @param buf data to encode * @throws java.nio.ReadOnlyBufferException if {@code dest} is read only * @throws java.nio.BufferOverflowException if {@code dest} overflows * @throws IllegalArgumentException if {@code buf} has more than 2^31 bytes remaining * @throws IllegalArgumentException if either parameter is null */ protected static void putByteBuffer(ByteBuffer dest, ByteBuffer buf) { Preconditions.checkArgument(dest != null, "null dest"); Preconditions.checkArgument(buf != null, "null buf"); UnsignedIntEncoder.write(dest, buf.remaining()); dest.put(buf.asReadOnlyBuffer()); } /** * Deserialize a {@link ByteBuffer} previously serialized by {@link #putByteBuffer putByteBuffer()} from the buffer. * * @param buf source for encoded data * @return decoded data * @throws java.nio.BufferUnderflowException if {@code buf} underflows * @throws IllegalArgumentException if input is bogus * @throws IllegalArgumentException if {@code buf} is null */ protected static ByteBuffer getByteBuffer(ByteBuffer buf) { Preconditions.checkArgument(buf != null, "null buf"); final int numBytes = UnsignedIntEncoder.read(buf); if (numBytes > buf.remaining()) throw new IllegalArgumentException("bogus buffer length " + numBytes + " > " + buf.remaining()); final ByteBuffer result = (ByteBuffer)buf.slice().limit(numBytes); buf.position(buf.position() + numBytes); return result; } protected static int calculateSize(ByteBuffer buf) { return UnsignedIntEncoder.encodeLength(buf.remaining()) + buf.remaining(); } /** * Serialize a {@link String} into the buffer. * * @param dest destination for encoded data * @param string string to encode * @throws java.nio.ReadOnlyBufferException if {@code dest} is read only * @throws java.nio.BufferOverflowException if {@code dest} overflows * @throws IllegalArgumentException if either parameter is null */ protected static void putString(ByteBuffer dest, String string) { Preconditions.checkArgument(dest != null, "null dest"); Preconditions.checkArgument(string != null, "null string"); for (int i = 0; i < string.length(); i++) { final int c = string.charAt(i); if (c >= 0x0001 && c <= 0x007f) dest.put((byte)c); else if (c < 0x0800) { dest.put((byte)(0xc0 | ((c >> 6) & 0x1f))); dest.put((byte)(0x80 | ((c >> 0) & 0x3f))); } else { dest.put((byte)(0xe0 | ((c >> 12) & 0x0f))); dest.put((byte)(0x80 | ((c >> 6) & 0x3f))); dest.put((byte)(0x80 | ((c >> 0) & 0x3f))); } } dest.put((byte)0); } /** * Deserialize a {@link String} previously serialized by {@link #putString putString()} from the buffer. * * @param buf source for encoded data * @return decoded string, never null * @throws java.nio.BufferUnderflowException if {@code buf} underflows * @throws IllegalArgumentException if input is bogus */ protected static String getString(ByteBuffer buf) { Preconditions.checkArgument(buf != null, "null buf"); final StringWriter writer = new StringWriter(); while (true) { final int b1 = buf.get() & 0xff; if (b1 == 0) break; if ((b1 & 0x80) == 0) writer.append((char)b1); else if ((b1 & 0xe0) == 0xc0) { final int b2 = buf.get() & 0xff; if ((b2 & 0xc0) != 0x80) throw new IllegalArgumentException("invalid UTF-8 sequence: " + b1 + " " + b2); writer.append((char)((b1 & 0x1f) << 6 | (b2 & 0x3f))); } else if ((b1 & 0xf0) == 0xe0) { final int b2 = buf.get() & 0xff; final int b3 = buf.get() & 0xff; if ((b2 & 0xc0) != 0x80 || (b3 & 0xc0) != 0x80) throw new IllegalArgumentException("invalid UTF-8 sequence: " + b1 + " " + b2 + " " + b3); writer.append((char)(((b1 & 0x0f) << 12) | ((b2 & 0x3f) << 6) | (b3 & 0x3f))); } else throw new IllegalArgumentException("invalid UTF-8 sequence: " + b1); } return writer.toString(); } protected static int calculateSize(String string) { Preconditions.checkArgument(string != null, "null string"); int total = 1; for (int i = 0; i < string.length(); i++) { final int ch = string.charAt(i); total += (ch != 0x0000 && ch < 0x0080) ? 1 : ch < 0x0800 ? 2 : 3; } return total; } /** * Serialize a boolean value into the buffer. * * @param dest destination for encoded data * @param value value to encode * @throws java.nio.ReadOnlyBufferException if {@code dest} is read only * @throws java.nio.BufferOverflowException if {@code dest} overflows * @throws IllegalArgumentException if {@code dest} is null */ protected static void putBoolean(ByteBuffer dest, boolean value) { Preconditions.checkArgument(dest != null, "null dest"); dest.put(value ? (byte)1 : (byte)0); } /** * Deserialize a boolean value previously serialized by {@link #putBoolean putBoolean()} from the buffer. * * @param buf source for encoded data * @return decoded value * @throws java.nio.BufferUnderflowException if {@code buf} underflows * @throws IllegalArgumentException if input is bogus */ protected static boolean getBoolean(ByteBuffer buf) { Preconditions.checkArgument(buf != null, "null buf"); switch (buf.get()) { case (byte)0: return false; case (byte)1: return true; default: throw new IllegalArgumentException("read invalid boolean value"); } } /** * Serialize a {@link Timestamp} value into the buffer. * * @param dest destination for encoded data * @param timestamp value to encode * @param version protocol encoding version * @throws java.nio.ReadOnlyBufferException if {@code dest} is read only * @throws java.nio.BufferOverflowException if {@code dest} overflows * @throws IllegalArgumentException if {@code dest} or {@code timestamp} is null */ protected static void putTimestamp(ByteBuffer dest, Timestamp timestamp, int version) { Preconditions.checkArgument(dest != null, "null dest"); Preconditions.checkArgument(timestamp != null, "null timestamp"); Message.checkVersion(version); if (version > Message.VERSION_2) LongEncoder.write(dest, timestamp.getMillis()); else UnsignedIntEncoder.write(dest, timestamp.getMillis()); } /** * Deserialize a {@link Timestamp} value previously serialized by {@link #putTimestamp putTimestamp()} from the buffer. * * @param buf source for encoded data * @param version protocol encoding version * @return decoded value * @throws java.nio.BufferUnderflowException if {@code buf} underflows * @throws IllegalArgumentException if input is bogus */ protected static Timestamp getTimestamp(ByteBuffer buf, int version) { Preconditions.checkArgument(buf != null, "null buf"); Message.checkVersion(version); return new Timestamp(version > VERSION_2 ? (int)LongEncoder.read(buf) : UnsignedIntEncoder.read(buf)); } protected static int calculateSize(Timestamp timestamp, int version) { Preconditions.checkArgument(timestamp != null, "null timestamp"); Message.checkVersion(version); final int millis = timestamp.getMillis(); return version > VERSION_2 ? LongEncoder.encodeLength(millis) : UnsignedIntEncoder.encodeLength(millis); } // Debugging String describe(ByteBuffer buf) { if (buf == null) return null; final int size = buf.remaining(); if (size <= 32) { final byte[] data = new byte[size]; buf.asReadOnlyBuffer().get(data); return ByteUtil.toString(data); } return size + " bytes"; } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy