/* * Copyright 2015 the original author or authors. * * 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 io.atomix.copycat.server.protocol; import io.atomix.catalyst.buffer.BufferInput; import io.atomix.catalyst.buffer.BufferOutput; import io.atomix.catalyst.serializer.Serializer; import io.atomix.catalyst.util.Assert; import io.atomix.copycat.protocol.AbstractRequest; import io.atomix.copycat.server.storage.entry.Entry; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; /** * Append entries request. * <p> * Append entries requests are at the core of the replication protocol. Leaders send append requests * to followers to replicate and commit log entries, and followers sent append requests to passive members * to replicate committed log entries. * * @author <a href="http://github.com/kuujo">Jordan Halterman</a> */ public class AppendRequest extends AbstractRequest { /** * Returns a new append request builder. * * @return A new append request builder. */ public static Builder builder() { return new Builder(new AppendRequest()); } /** * Returns an append request builder for an existing request. * * @param request The request to build. * @return The append request builder. */ public static Builder builder(AppendRequest request) { return new Builder(request); } private long term; private int leader; private long logIndex; private long logTerm; private List<Entry> entries; private long commitIndex = -1; private long globalIndex = -1; /** * Returns the requesting node's current term. * * @return The requesting node's current term. */ public long term() { return term; } /** * Returns the requesting leader address. * * @return The leader's address. */ public int leader() { return leader; } /** * Returns the index of the log entry preceding the new entry. * * @return The index of the log entry preceding the new entry. */ public long logIndex() { return logIndex; } /** * Returns the term of the log entry preceding the new entry. * * @return The index of the term preceding the new entry. */ public long logTerm() { return logTerm; } /** * Returns the log entries to append. * * @return A list of log entries. */ public List<? extends Entry> entries() { return entries; } /** * Returns the leader's commit index. * * @return The leader commit index. */ public long commitIndex() { return commitIndex; } /** * Returns the leader's global index. * * @return The leader global index. */ public long globalIndex() { return globalIndex; } @Override public void writeObject(BufferOutput<?> buffer, Serializer serializer) { buffer.writeLong(term) .writeInt(leader) .writeLong(logIndex) .writeLong(logTerm) .writeLong(commitIndex) .writeLong(globalIndex); buffer.writeInt(entries.size()); for (Entry entry : entries) { buffer.writeLong(entry.getIndex()).writeLong(entry.getTerm()); serializer.writeObject(entry, buffer); } } @Override public void readObject(BufferInput<?> buffer, Serializer serializer) { term = buffer.readLong(); leader = buffer.readInt(); logIndex = buffer.readLong(); logTerm = buffer.readLong(); commitIndex = buffer.readLong(); globalIndex = buffer.readLong(); int numEntries = buffer.readInt(); entries = new ArrayList<>(numEntries); for (int i = 0; i < numEntries; i++) { long index = buffer.readLong(); long term = buffer.readLong(); Entry entry = serializer.readObject(buffer); entry.setIndex(index).setTerm(term); entries.add(entry); } } @Override public int hashCode() { return Objects.hash(getClass(), term, leader, logIndex, logTerm, entries, commitIndex, globalIndex); } @Override public boolean equals(Object object) { if (object instanceof AppendRequest) { AppendRequest request = (AppendRequest) object; return request.term == term && request.leader == leader && request.logIndex == logIndex && request.logTerm == logTerm && request.entries.equals(entries) && request.commitIndex == commitIndex && request.globalIndex == globalIndex; } return false; } @Override public String toString() { return String.format("%s[term=%d, leader=%s, logIndex=%d, logTerm=%d, entries=[%d], commitIndex=%d, globalIndex=%d]", getClass().getSimpleName(), term, leader, logIndex, logTerm, entries.size(), commitIndex, globalIndex); } /** * Append request builder. */ public static class Builder extends AbstractRequest.Builder<Builder, AppendRequest> { protected Builder(AppendRequest request) { super(request); } /** * Sets the request term. * * @param term The request term. * @return The append request builder. * @throws IllegalArgumentException if the {@code term} is not positive */ public Builder withTerm(long term) { request.term = Assert.arg(term, term > 0, "term must be positive"); return this; } /** * Sets the request leader. * * @param leader The request leader. * @return The append request builder. * @throws IllegalArgumentException if the {@code leader} is not positive */ public Builder withLeader(int leader) { request.leader = leader; return this; } /** * Sets the request last log index. * * @param index The request last log index. * @return The append request builder. * @throws IllegalArgumentException if the {@code index} is not positive */ public Builder withLogIndex(long index) { request.logIndex = Assert.argNot(index, index < 0, "log index must be not be negative"); return this; } /** * Sets the request last log term. * * @param term The request last log term. * @return The append request builder. * @throws IllegalArgumentException if the {@code term} is not positive */ public Builder withLogTerm(long term) { request.logTerm = Assert.argNot(term, term < 0, "term must be positive"); return this; } /** * Sets the request entries. * * @param entries The request entries. * @return The append request builder. * @throws NullPointerException if {@code entries} is null */ public Builder withEntries(Entry... entries) { return withEntries(Arrays.asList(Assert.notNull(entries, "entries"))); } /** * Sets the request entries. * * @param entries The request entries. * @return The append request builder. * @throws NullPointerException if {@code entries} is null */ @SuppressWarnings("unchecked") public Builder withEntries(List<? extends Entry> entries) { request.entries = (List<Entry>) Assert.notNull(entries, "entries"); return this; } /** * Adds an entry to the request. * * @param entry The entry to add. * @return The request builder. * @throws NullPointerException if {@code entry} is {@code null} */ public Builder addEntry(Entry entry) { request.entries.add(Assert.notNull(entry, "entry")); return this; } /** * Sets the request commit index. * * @param index The request commit index. * @return The append request builder. * @throws IllegalArgumentException if index is not positive */ public Builder withCommitIndex(long index) { request.commitIndex = Assert.argNot(index, index < 0, "commit index must not be negative"); return this; } /** * Sets the request global index. * * @param index The request global index. * @return The append request builder. * @throws IllegalArgumentException if index is not positive */ public Builder withGlobalIndex(long index) { request.globalIndex = Assert.argNot(index, index < 0, "global index must not be negative"); return this; } /** * @throws IllegalStateException if the term, log term, log index, commit index, or global index are not positive, or * if entries is null */ @Override public AppendRequest build() { super.build(); Assert.stateNot(request.term <= 0, "term must be positive"); Assert.stateNot(request.logIndex < 0, "log index must not be negative"); if (request.logIndex > 0) { Assert.stateNot(request.logTerm == 0, "log term must be specified"); } else { Assert.stateNot(request.logTerm < 0, "log term must not be negative"); } Assert.stateNot(request.entries == null, "entries cannot be null"); Assert.stateNot(request.commitIndex < 0, "commit index must not be negative"); Assert.stateNot(request.globalIndex < 0, "global index must not be negative"); return request; } @Override public int hashCode() { return Objects.hash(request); } @Override public boolean equals(Object object) { return object instanceof Builder && ((Builder) object).request.equals(request); } @Override public String toString() { return String.format("%s[request=%s]", getClass().getCanonicalName(), request); } } }