├── .gitignore ├── LICENSE ├── README.md ├── core ├── pom.xml └── src │ ├── main │ └── java │ │ └── net │ │ └── data │ │ └── technology │ │ └── jraft │ │ ├── ClusterConfiguration.java │ │ ├── ClusterServer.java │ │ ├── LogEntry.java │ │ ├── LogValueType.java │ │ ├── Logger.java │ │ ├── LoggerFactory.java │ │ ├── PeerServer.java │ │ ├── RaftClient.java │ │ ├── RaftConsensus.java │ │ ├── RaftContext.java │ │ ├── RaftMessage.java │ │ ├── RaftMessageHandler.java │ │ ├── RaftMessageSender.java │ │ ├── RaftMessageType.java │ │ ├── RaftParameters.java │ │ ├── RaftRequestMessage.java │ │ ├── RaftResponseMessage.java │ │ ├── RaftServer.java │ │ ├── RpcClient.java │ │ ├── RpcClientFactory.java │ │ ├── RpcException.java │ │ ├── RpcListener.java │ │ ├── SequentialLogStore.java │ │ ├── ServerRole.java │ │ ├── ServerState.java │ │ ├── ServerStateManager.java │ │ ├── Snapshot.java │ │ ├── SnapshotSyncContext.java │ │ ├── SnapshotSyncRequest.java │ │ └── StateMachine.java │ └── test │ └── java │ └── net │ └── data │ └── technology │ └── jraft │ └── tests │ ├── ClusterConfigurationTests.java │ └── SnapshotSyncRequestTests.java ├── dmprinter ├── pom.xml └── src │ └── main │ └── java │ ├── jraft.json │ ├── log4j.properties │ └── net │ └── data │ └── technology │ └── jraft │ ├── App.java │ ├── DummyMessageHandler.java │ └── MessagePrinter.java ├── exts ├── pom.xml └── src │ ├── main │ └── java │ │ └── net │ │ └── data │ │ └── technology │ │ └── jraft │ │ └── extensions │ │ ├── AsyncUtility.java │ │ ├── BinaryUtils.java │ │ ├── FileBasedSequentialLogStore.java │ │ ├── FileBasedServerStateManager.java │ │ ├── H2LogStore.java │ │ ├── Log4jLogger.java │ │ ├── Log4jLoggerFactory.java │ │ ├── Pair.java │ │ ├── RpcTcpClient.java │ │ ├── RpcTcpClientFactory.java │ │ ├── RpcTcpListener.java │ │ └── http │ │ ├── HttpRpcClient.java │ │ ├── HttpRpcClientFactory.java │ │ ├── JraftServlet.java │ │ └── JraftServletListener.java │ └── test │ └── java │ └── net │ └── data │ └── technology │ └── jraft │ └── extensions │ ├── BinaryUtilTests.java │ ├── FileBasedSequentialLogStoreTests.java │ ├── FileBasedServerStateManagerTests.java │ ├── GsonSerializationTests.java │ └── H2LogStoreTests.java ├── pom.xml └── setup ├── addsrv.bat ├── cleanup.bat ├── init-cluster.json ├── setup.bat └── startsrv.bat /.gitignore: -------------------------------------------------------------------------------- 1 | **/*.class 2 | **/*.classpath 3 | **/*.project 4 | **/*.jar 5 | exts/target/ 6 | core/target/ 7 | dmprinter/target/ 8 | .settings/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 andy.yx.chen@outlook.com 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This project will be proactively monitored and updated** 2 | 3 | # jraft 4 | Raft consensus implementation in java 5 | 6 | The core algorithm is implemented based on the TLA+ spec, whose safety is proven, and liveness is highly dependent on the pseudo-random number sequence, which could be fine if different servers in the same cluster are generating random numbers with different seeds. 7 | 8 | ## Supported Features 9 | - [x] Core Algorithm, safety is proven 10 | - [x] Configuration Change Support, add or remove servers one by one without limitation 11 | - [x] Client Request Support 12 | - [x] **Urgent commit**, see below 13 | - [x] log compaction 14 | 15 | ### Urgent Commit 16 | **Urgent Commit** is a new feature introduced by this implementation, which enables the **leader** to ask all other servers to commit one or more logs if the commit index is advanced. With Urgent Commit, the system's performance is highly improved and the heartbeat interval could be increased to seconds, depending on how long your application can abide when a leader goes down, usually, one or two seconds is fine. 17 | 18 | ## About this implementation 19 | It's always safer to implement such kind of algorithm based on Math description other than natural language description. 20 | there should be an auto-conversion from TLA+ to programming languages, even they are taking things in different ways, but they are identical. 21 | 22 | In the example of **dmprinter** (Distributed Message Printer), it takes about 4ms to commit a message, while in **Active-Active** scenario (sending messages to all three instances of dmprinter), it takes about 9ms to commit a message, the data is collected by CLT (Central Limitation Theory) with 95% of confidence level. 23 | 24 | ## Code Structure 25 | This project contains not that much code, as it's well abstracted, here is the project structure 26 | * **core**, the core algorithm implementation, you can go only with this, however, you need to implement the following interfaces, 27 | 1. **Logger** and **LoggerFactory** 28 | 2. **RpcClient** and **RpcClientFactory** 29 | 3. **RpcListener** 30 | 4. **ServerStateManager** and **SequentialLogStore** 31 | 5. **StateMachine** 32 | * **exts**, some implementations for the interfaces mentioned above, it provides TCP based CompletableFuture enabled RPC client and server as well as **FileBasedSequentialLogStore**, with this, you will be able to implement your own system by only implement **StateMachine** interface 33 | * **dmprinter**, a sample application, as it's name, it's distributed message printer, for sample and testing. 34 | * **setup**, some scripts for Windows(R) platform to run **dmprinter** 35 | 36 | ## Run dmprinter 37 | **dmprinter** is a distributed message printer which is used to verify the core implementation. A set of Windows(R) based setup scripts is shipped with this project, under **setup** folder. 38 | 1. Export three projects into one executable jar file, called jraft.jar 39 | 2. Start a command prompt, change directory to **setup** folder 40 | 3. Run **setup.cmd**, it will start three instances of jraft 41 | 4. Write a simple client by using Python or whatever language you would love to connect to any port between `8001` and `8003`, which dmprinter is listening on. **message format** see below 42 | 5. You can call `addsrv.cmd \` to start new instance of raft and call `addsrv:\,tcp://localhost:900\` to join the server to cluster, e.g. `addsrv:4,tcp://localhost:9004` through the client, or remove a server from cluster `rmsrv,4`, check out more details about the message format as below, 43 | 44 | > Message format \\ 45 | 46 | > \ := four bytes, which is encoded from an integer in little-endian format, the integer is the bytes of the \ 47 | 48 | > \ := \:\ 49 | 50 | > \ := uuid 51 | 52 | > \ := addsrv|rmsrv 53 | 54 | > \ := srvid,uri|srvid|any-string 55 | -------------------------------------------------------------------------------- /core/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | 6 | net.data-technology.jraft 7 | jraft-root 8 | 1.0 9 | 10 | 11 | net.data-technology.jraft 12 | jraft-core 13 | 1.0.0 14 | jar 15 | 16 | jraft-core 17 | https://github.com/datatechnology/jraft 18 | Java implementation of Raft Consensus, this module contains only the core algorithm, and does not have any dependency except Java SE 8, for interface implementations that required by this module, please checkout jraft-ext module 19 | 20 | 21 | 22 | Apache License, Version 2.0 23 | http://www.apache.org/licenses/LICENSE-2.0.txt 24 | repo 25 | 26 | 27 | 28 | 29 | 30 | techadmin@data-technology.net 31 | datatech 32 | Data Technology LLC 33 | http://www.data-technology.net 34 | 35 | 36 | 37 | 38 | https://github.com/datatechnology/jraft.git 39 | 40 | 41 | 42 | UTF-8 43 | 44 | 45 | 46 | 47 | junit 48 | junit 49 | test 50 | 51 | 52 | 53 | 54 | 55 | ossrh 56 | https://oss.sonatype.org/content/repositories/snapshots 57 | 58 | 59 | 60 | 61 | 62 | 63 | org.apache.maven.plugins 64 | maven-compiler-plugin 65 | 3.5.1 66 | 67 | 1.8 68 | 1.8 69 | 70 | 71 | 72 | org.sonatype.plugins 73 | nexus-staging-maven-plugin 74 | 1.6.7 75 | true 76 | 77 | ossrh 78 | https://oss.sonatype.org/ 79 | true 80 | 81 | 82 | 83 | org.apache.maven.plugins 84 | maven-source-plugin 85 | 2.2.1 86 | 87 | 88 | attach-sources 89 | 90 | jar-no-fork 91 | 92 | 93 | 94 | 95 | 96 | org.apache.maven.plugins 97 | maven-javadoc-plugin 98 | 2.9.1 99 | 100 | 101 | attach-javadocs 102 | 103 | jar 104 | 105 | 106 | 107 | 108 | 109 | org.apache.maven.plugins 110 | maven-gpg-plugin 111 | 1.5 112 | 113 | 114 | sign-artifacts 115 | verify 116 | 117 | sign 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/ClusterConfiguration.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | import java.nio.ByteBuffer; 21 | import java.util.ArrayList; 22 | import java.util.LinkedList; 23 | import java.util.List; 24 | 25 | /** 26 | * Cluster configuration, a class to hold the cluster configuration information 27 | * @author Data Technology LLC 28 | * 29 | */ 30 | public class ClusterConfiguration { 31 | 32 | private long logIndex; 33 | private long lastLogIndex; 34 | private List servers; 35 | 36 | public ClusterConfiguration(){ 37 | this.servers = new LinkedList(); 38 | this.logIndex = 0; 39 | this.lastLogIndex = 0; 40 | } 41 | 42 | /** 43 | * De-serialize the data stored in buffer to cluster configuration 44 | * this is used for the peers to get the cluster configuration from log entry value 45 | * @param buffer the binary data 46 | * @return cluster configuration 47 | */ 48 | public static ClusterConfiguration fromBytes(ByteBuffer buffer){ 49 | ClusterConfiguration configuration = new ClusterConfiguration(); 50 | configuration.setLogIndex(buffer.getLong()); 51 | configuration.setLastLogIndex(buffer.getLong()); 52 | while(buffer.hasRemaining()){ 53 | configuration.getServers().add(new ClusterServer(buffer)); 54 | } 55 | 56 | return configuration; 57 | } 58 | 59 | /** 60 | * De-serialize the data stored in buffer to cluster configuration 61 | * this is used for the peers to get the cluster configuration from log entry value 62 | * @param data the binary data 63 | * @return cluster configuration 64 | */ 65 | public static ClusterConfiguration fromBytes(byte[] data){ 66 | return fromBytes(ByteBuffer.wrap(data)); 67 | } 68 | 69 | public long getLogIndex() { 70 | return logIndex; 71 | } 72 | 73 | public void setLogIndex(long logIndex) { 74 | this.logIndex = logIndex; 75 | } 76 | 77 | /** 78 | * Gets the log index that contains the previous cluster configuration 79 | * @return log index 80 | */ 81 | public long getLastLogIndex() { 82 | return lastLogIndex; 83 | } 84 | 85 | public void setLastLogIndex(long lastLogIndex) { 86 | this.lastLogIndex = lastLogIndex; 87 | } 88 | 89 | public List getServers() { 90 | return servers; 91 | } 92 | 93 | /** 94 | * Try to get a cluster server configuration from cluster configuration 95 | * @param id the server id 96 | * @return a cluster server configuration or null if id is not found 97 | */ 98 | public ClusterServer getServer(int id){ 99 | for(ClusterServer server : this.servers){ 100 | if(server.getId() == id){ 101 | return server; 102 | } 103 | } 104 | 105 | return null; 106 | } 107 | 108 | /** 109 | * Serialize the cluster configuration into a buffer 110 | * this is used for the leader to serialize a new cluster configuration and replicate to peers 111 | * @return binary data that represents the cluster configuration 112 | */ 113 | public byte[] toBytes(){ 114 | int totalSize = Long.BYTES * 2; 115 | List serversData = new ArrayList(this.servers.size()); 116 | for(int i = 0; i < this.servers.size(); ++i){ 117 | ClusterServer server = this.servers.get(i); 118 | byte[] dataForServer = server.toBytes(); 119 | totalSize += dataForServer.length; 120 | serversData.add(dataForServer); 121 | } 122 | 123 | ByteBuffer buffer = ByteBuffer.allocate(totalSize); 124 | buffer.putLong(this.logIndex); 125 | buffer.putLong(this.lastLogIndex); 126 | for(int i = 0; i < serversData.size(); ++i){ 127 | buffer.put(serversData.get(i)); 128 | } 129 | 130 | return buffer.array(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/ClusterServer.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | import java.nio.ByteBuffer; 21 | import java.nio.charset.StandardCharsets; 22 | 23 | /** 24 | * Cluster server configuration 25 | * a class to hold the configuration information for a server in a cluster 26 | * @author Data Technology LLC 27 | * 28 | */ 29 | public class ClusterServer { 30 | 31 | private int id; 32 | private String endpoint; 33 | 34 | public ClusterServer(){ 35 | this.id = -1; 36 | this.endpoint = null; 37 | } 38 | 39 | public ClusterServer(ByteBuffer buffer){ 40 | this.id = buffer.getInt(); 41 | int dataSize = buffer.getInt(); 42 | byte[] endpointData = new byte[dataSize]; 43 | buffer.get(endpointData); 44 | this.endpoint = new String(endpointData, StandardCharsets.UTF_8); 45 | } 46 | 47 | public int getId() { 48 | return id; 49 | } 50 | 51 | public void setId(int id) { 52 | this.id = id; 53 | } 54 | 55 | public String getEndpoint() { 56 | return endpoint; 57 | } 58 | 59 | public void setEndpoint(String endpoint) { 60 | this.endpoint = endpoint; 61 | } 62 | 63 | /** 64 | * Serialize a server configuration to binary data 65 | * @return the binary data that represents the server configuration 66 | */ 67 | public byte[] toBytes(){ 68 | byte[] endpointData = this.endpoint.getBytes(StandardCharsets.UTF_8); 69 | ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES * 2 + endpointData.length); 70 | buffer.putInt(this.id); 71 | buffer.putInt(endpointData.length); 72 | buffer.put(endpointData); 73 | return buffer.array(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/LogEntry.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | /** 21 | * Log entry, which represents an entry stored in the sequential log store 22 | * replication is done through LogEntry objects 23 | * @author Data Technology LLC 24 | * 25 | */ 26 | public class LogEntry { 27 | 28 | private byte[] value; 29 | private long term; 30 | private LogValueType vaueType; 31 | 32 | public LogEntry(){ 33 | this(0, null); 34 | } 35 | 36 | public LogEntry(long term, byte[] value){ 37 | this(term, value, LogValueType.Application); 38 | } 39 | 40 | public LogEntry(long term, byte[] value, LogValueType valueType){ 41 | this.term = term; 42 | this.value = value; 43 | this.vaueType = valueType; 44 | } 45 | 46 | public long getTerm(){ 47 | return this.term; 48 | } 49 | 50 | public byte[] getValue(){ 51 | return this.value; 52 | } 53 | 54 | public LogValueType getValueType(){ 55 | return this.vaueType; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/LogValueType.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | /** 21 | * Log value type for the value of a log entry 22 | * @author Data Technology LLC 23 | * 24 | */ 25 | public enum LogValueType { 26 | 27 | /** 28 | * Log value for application, which means the value could only be understood by the application (not jraft) 29 | */ 30 | Application { 31 | @Override 32 | public byte toByte(){ 33 | return 1; 34 | } 35 | }, 36 | 37 | /** 38 | * Log value is cluster configuration data 39 | */ 40 | Configuration { 41 | @Override 42 | public byte toByte(){ 43 | return 2; 44 | } 45 | }, 46 | 47 | /** 48 | * Log value is cluster server id 49 | */ 50 | ClusterServer { 51 | @Override 52 | public byte toByte(){ 53 | return 3; 54 | } 55 | }, 56 | 57 | /** 58 | * Log value is a pack of many log entries, this is used when a server is left far behind or a new server just join the cluster 59 | */ 60 | LogPack { 61 | @Override 62 | public byte toByte(){ 63 | return 4; 64 | } 65 | }, 66 | 67 | /** 68 | * Log value is snapshot sync request data 69 | */ 70 | SnapshotSyncRequest { 71 | @Override 72 | public byte toByte(){ 73 | return 5; 74 | } 75 | }; 76 | 77 | /** 78 | * Converts a LogValueType to a byte value 79 | * @return byte value of the LogValueType 80 | */ 81 | public abstract byte toByte(); 82 | 83 | /** 84 | * Converts a byte value to LogValueType 85 | * @param b byte value 86 | * @return LogValueType 87 | */ 88 | public static LogValueType fromByte(byte b){ 89 | switch(b){ 90 | case 1: 91 | return Application; 92 | case 2: 93 | return Configuration; 94 | case 3: 95 | return ClusterServer; 96 | case 4: 97 | return LogPack; 98 | case 5: 99 | return SnapshotSyncRequest; 100 | default: 101 | throw new IllegalArgumentException(String.format("%d is not defined for LogValueType", b)); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/Logger.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | public interface Logger { 21 | 22 | public void debug(String format, Object... args); 23 | 24 | public void info(String format, Object... args); 25 | 26 | public void warning(String format, Object... args); 27 | 28 | public void error(String format, Object... args); 29 | 30 | public void error(String format, Throwable error, Object... args); 31 | } 32 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/LoggerFactory.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | public interface LoggerFactory { 21 | 22 | public Logger getLogger(Class clazz); 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/PeerServer.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | import java.util.concurrent.Callable; 21 | import java.util.concurrent.CompletableFuture; 22 | import java.util.concurrent.Executor; 23 | import java.util.concurrent.ScheduledFuture; 24 | import java.util.concurrent.atomic.AtomicInteger; 25 | import java.util.function.Consumer; 26 | 27 | /** 28 | * Peer server in the same cluster for local server 29 | * this represents a peer for local server, it could be a leader, however, if local server is not a leader, though it has a list of peer servers, they are not used 30 | * @author Data Technology LLC 31 | * 32 | */ 33 | public class PeerServer { 34 | 35 | private ClusterServer clusterConfig; 36 | private RpcClient rpcClient; 37 | private int currentHeartbeatInterval; 38 | private int heartbeatInterval; 39 | private int rpcBackoffInterval; 40 | private int maxHeartbeatInterval; 41 | private AtomicInteger busyFlag; 42 | private AtomicInteger pendingCommitFlag; 43 | private Callable heartbeatTimeoutHandler; 44 | private ScheduledFuture heartbeatTask; 45 | private long nextLogIndex; 46 | private long matchedIndex; 47 | private boolean heartbeatEnabled; 48 | private SnapshotSyncContext snapshotSyncContext; 49 | private Executor executor; 50 | 51 | public PeerServer(ClusterServer server, RaftContext context, final Consumer heartbeatConsumer){ 52 | this.clusterConfig = server; 53 | this.rpcClient = context.getRpcClientFactory().createRpcClient(server.getEndpoint()); 54 | this.busyFlag = new AtomicInteger(0); 55 | this.pendingCommitFlag = new AtomicInteger(0); 56 | this.heartbeatInterval = this.currentHeartbeatInterval = context.getRaftParameters().getHeartbeatInterval(); 57 | this.maxHeartbeatInterval = context.getRaftParameters().getMaxHeartbeatInterval(); 58 | this.rpcBackoffInterval = context.getRaftParameters().getRpcFailureBackoff(); 59 | this.heartbeatTask = null; 60 | this.snapshotSyncContext = null; 61 | this.nextLogIndex = 1; 62 | this.matchedIndex = 0; 63 | this.heartbeatEnabled = false; 64 | this.executor = context.getScheduledExecutor(); 65 | PeerServer self = this; 66 | this.heartbeatTimeoutHandler = new Callable(){ 67 | 68 | @Override 69 | public Void call() throws Exception { 70 | heartbeatConsumer.accept(self); 71 | return null; 72 | }}; 73 | } 74 | 75 | public int getId(){ 76 | return this.clusterConfig.getId(); 77 | } 78 | 79 | public ClusterServer getClusterConfig(){ 80 | return this.clusterConfig; 81 | } 82 | 83 | public Callable getHeartbeartHandler(){ 84 | return this.heartbeatTimeoutHandler; 85 | } 86 | 87 | public synchronized int getCurrentHeartbeatInterval(){ 88 | return this.currentHeartbeatInterval; 89 | } 90 | 91 | public void setHeartbeatTask(ScheduledFuture heartbeatTask){ 92 | this.heartbeatTask = heartbeatTask; 93 | } 94 | 95 | public ScheduledFuture getHeartbeatTask(){ 96 | return this.heartbeatTask; 97 | } 98 | 99 | public boolean makeBusy(){ 100 | return this.busyFlag.compareAndSet(0, 1); 101 | } 102 | 103 | public void setFree(){ 104 | this.busyFlag.set(0); 105 | } 106 | 107 | public boolean isHeartbeatEnabled(){ 108 | return this.heartbeatEnabled; 109 | } 110 | 111 | public void enableHeartbeat(boolean enable){ 112 | this.heartbeatEnabled = enable; 113 | 114 | if(!enable){ 115 | this.heartbeatTask = null; 116 | } 117 | } 118 | 119 | public long getNextLogIndex() { 120 | return nextLogIndex; 121 | } 122 | 123 | public void setNextLogIndex(long nextLogIndex) { 124 | this.nextLogIndex = nextLogIndex; 125 | } 126 | 127 | public long getMatchedIndex(){ 128 | return this.matchedIndex; 129 | } 130 | 131 | public void setMatchedIndex(long matchedIndex){ 132 | this.matchedIndex = matchedIndex; 133 | } 134 | 135 | public void setPendingCommit(){ 136 | this.pendingCommitFlag.set(1); 137 | } 138 | 139 | public boolean clearPendingCommit(){ 140 | return this.pendingCommitFlag.compareAndSet(1, 0); 141 | } 142 | 143 | public void setSnapshotInSync(Snapshot snapshot){ 144 | if(snapshot == null){ 145 | this.snapshotSyncContext = null; 146 | }else{ 147 | this.snapshotSyncContext = new SnapshotSyncContext(snapshot); 148 | } 149 | } 150 | 151 | public SnapshotSyncContext getSnapshotSyncContext(){ 152 | return this.snapshotSyncContext; 153 | } 154 | 155 | public CompletableFuture SendRequest(RaftRequestMessage request){ 156 | boolean isAppendRequest = request.getMessageType() == RaftMessageType.AppendEntriesRequest || request.getMessageType() == RaftMessageType.InstallSnapshotRequest; 157 | return this.rpcClient.send(request) 158 | .thenComposeAsync((RaftResponseMessage response) -> { 159 | if(isAppendRequest){ 160 | this.setFree(); 161 | } 162 | 163 | this.resumeHeartbeatingSpeed(); 164 | return CompletableFuture.completedFuture(response); 165 | }, this.executor) 166 | .exceptionally((Throwable error) -> { 167 | if(isAppendRequest){ 168 | this.setFree(); 169 | } 170 | 171 | this.slowDownHeartbeating(); 172 | throw new RpcException(error, request); 173 | }); 174 | } 175 | 176 | public synchronized void slowDownHeartbeating(){ 177 | this.currentHeartbeatInterval = Math.min(this.maxHeartbeatInterval, this.currentHeartbeatInterval + this.rpcBackoffInterval); 178 | } 179 | 180 | public synchronized void resumeHeartbeatingSpeed(){ 181 | if(this.currentHeartbeatInterval > this.heartbeatInterval){ 182 | this.currentHeartbeatInterval = this.heartbeatInterval; 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/RaftClient.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | import java.nio.ByteBuffer; 21 | import java.util.Calendar; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | import java.util.Random; 25 | import java.util.Timer; 26 | import java.util.TimerTask; 27 | import java.util.concurrent.CompletableFuture; 28 | 29 | public class RaftClient { 30 | 31 | private Map rpcClients = new HashMap(); 32 | private RpcClientFactory rpcClientFactory; 33 | private ClusterConfiguration configuration; 34 | private Logger logger; 35 | private Timer timer; 36 | private int leaderId; 37 | private boolean randomLeader; 38 | private Random random; 39 | 40 | public RaftClient(RpcClientFactory rpcClientFactory, ClusterConfiguration configuration, LoggerFactory loggerFactory){ 41 | this.random = new Random(Calendar.getInstance().getTimeInMillis()); 42 | this.rpcClientFactory = rpcClientFactory; 43 | this.configuration = configuration; 44 | this.leaderId = configuration.getServers().get(this.random.nextInt(configuration.getServers().size())).getId(); 45 | this.randomLeader = true; 46 | this.logger = loggerFactory.getLogger(getClass()); 47 | this.timer = new Timer(); 48 | } 49 | 50 | public CompletableFuture appendEntries(byte[][] values){ 51 | if(values == null || values.length == 0){ 52 | throw new IllegalArgumentException("values cannot be null or empty"); 53 | } 54 | 55 | LogEntry[] logEntries = new LogEntry[values.length]; 56 | for(int i = 0; i < values.length; ++i){ 57 | logEntries[i] = new LogEntry(0, values[i]); 58 | } 59 | 60 | RaftRequestMessage request = new RaftRequestMessage(); 61 | request.setMessageType(RaftMessageType.ClientRequest); 62 | request.setLogEntries(logEntries); 63 | 64 | CompletableFuture result = new CompletableFuture(); 65 | this.tryCurrentLeader(request, result, 0, 0); 66 | return result; 67 | } 68 | 69 | public CompletableFuture addServer(ClusterServer server){ 70 | if(server == null){ 71 | throw new IllegalArgumentException("server cannot be null"); 72 | } 73 | 74 | LogEntry[] logEntries = new LogEntry[1]; 75 | logEntries[0] = new LogEntry(0, server.toBytes(), LogValueType.ClusterServer); 76 | RaftRequestMessage request = new RaftRequestMessage(); 77 | request.setMessageType(RaftMessageType.AddServerRequest); 78 | request.setLogEntries(logEntries); 79 | 80 | CompletableFuture result = new CompletableFuture(); 81 | this.tryCurrentLeader(request, result, 0, 0); 82 | return result; 83 | } 84 | 85 | public CompletableFuture removeServer(int serverId){ 86 | if(serverId < 0){ 87 | throw new IllegalArgumentException("serverId must be equal or greater than zero"); 88 | } 89 | 90 | ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES); 91 | buffer.putInt(serverId); 92 | LogEntry[] logEntries = new LogEntry[1]; 93 | logEntries[0] = new LogEntry(0, buffer.array(), LogValueType.ClusterServer); 94 | RaftRequestMessage request = new RaftRequestMessage(); 95 | request.setMessageType(RaftMessageType.RemoveServerRequest); 96 | request.setLogEntries(logEntries); 97 | 98 | CompletableFuture result = new CompletableFuture(); 99 | this.tryCurrentLeader(request, result, 0, 0); 100 | return result; 101 | } 102 | 103 | private void tryCurrentLeader(RaftRequestMessage request, CompletableFuture future, int rpcBackoff, int retry){ 104 | logger.debug("trying request to %d as current leader", this.leaderId); 105 | getOrCreateRpcClient().send(request).whenCompleteAsync((RaftResponseMessage response, Throwable error) -> { 106 | if(error == null){ 107 | logger.debug("response from remote server, leader: %d, accepted: %s", response.getDestination(), String.valueOf(response.isAccepted())); 108 | if(response.isAccepted()){ 109 | future.complete(true); 110 | }else{ 111 | // set the leader return from the server 112 | if(this.leaderId == response.getDestination() && !this.randomLeader){ 113 | future.complete(false); 114 | }else{ 115 | this.randomLeader = false; 116 | this.leaderId = response.getDestination(); 117 | tryCurrentLeader(request, future, rpcBackoff, retry); 118 | } 119 | } 120 | }else{ 121 | logger.info("rpc error, failed to send request to remote server (%s)", error.getMessage()); 122 | if(retry > configuration.getServers().size()){ 123 | future.complete(false); 124 | return; 125 | } 126 | 127 | // try a random server as leader 128 | this.leaderId = this.configuration.getServers().get(this.random.nextInt(this.configuration.getServers().size())).getId(); 129 | this.randomLeader = true; 130 | refreshRpcClient(); 131 | 132 | if(rpcBackoff > 0){ 133 | timer.schedule(new TimerTask(){ 134 | 135 | @Override 136 | public void run() { 137 | tryCurrentLeader(request, future, rpcBackoff + 50, retry + 1); 138 | 139 | }}, rpcBackoff); 140 | }else{ 141 | tryCurrentLeader(request, future, rpcBackoff + 50, retry + 1); 142 | } 143 | } 144 | }); 145 | } 146 | 147 | private RpcClient getOrCreateRpcClient(){ 148 | synchronized(this.rpcClients){ 149 | if(this.rpcClients.containsKey(this.leaderId)){ 150 | return this.rpcClients.get(this.leaderId); 151 | } 152 | 153 | RpcClient client = this.rpcClientFactory.createRpcClient(getLeaderEndpoint()); 154 | this.rpcClients.put(this.leaderId, client); 155 | return client; 156 | } 157 | } 158 | 159 | private RpcClient refreshRpcClient(){ 160 | synchronized(this.rpcClients){ 161 | RpcClient client = this.rpcClientFactory.createRpcClient(getLeaderEndpoint()); 162 | this.rpcClients.put(this.leaderId, client); 163 | return client; 164 | } 165 | } 166 | 167 | private String getLeaderEndpoint(){ 168 | for(ClusterServer server : this.configuration.getServers()){ 169 | if(server.getId() == this.leaderId){ 170 | return server.getEndpoint(); 171 | } 172 | } 173 | 174 | logger.info("no endpoint could be found for leader %d, that usually means no leader is elected, retry the first one", this.leaderId); 175 | this.randomLeader = true; 176 | this.leaderId = this.configuration.getServers().get(0).getId(); 177 | return this.configuration.getServers().get(0).getEndpoint(); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/RaftConsensus.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | public class RaftConsensus { 21 | 22 | public static RaftMessageSender run(RaftContext context){ 23 | if(context == null){ 24 | throw new IllegalArgumentException("context cannot be null"); 25 | } 26 | 27 | RaftServer server = new RaftServer(context); 28 | RaftMessageSender messageSender = server.createMessageSender(); 29 | context.getStateMachine().start(messageSender); 30 | context.getRpcListener().startListening(server); 31 | return messageSender; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/RaftContext.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | import java.util.concurrent.ScheduledThreadPoolExecutor; 21 | 22 | public class RaftContext { 23 | 24 | private ServerStateManager serverStateManager; 25 | private RpcListener rpcListener; 26 | private LoggerFactory loggerFactory; 27 | private RpcClientFactory rpcClientFactory; 28 | private StateMachine stateMachine; 29 | private RaftParameters raftParameters; 30 | private ScheduledThreadPoolExecutor scheduledExecutor; 31 | 32 | public RaftContext(ServerStateManager stateManager, StateMachine stateMachine, RaftParameters raftParameters, RpcListener rpcListener, LoggerFactory logFactory, RpcClientFactory rpcClientFactory){ 33 | this(stateManager, stateMachine, raftParameters, rpcListener, logFactory, rpcClientFactory, null); 34 | } 35 | 36 | public RaftContext(ServerStateManager stateManager, StateMachine stateMachine, RaftParameters raftParameters, RpcListener rpcListener, LoggerFactory logFactory, RpcClientFactory rpcClientFactory, ScheduledThreadPoolExecutor scheduledExecutor){ 37 | this.serverStateManager = stateManager; 38 | this.stateMachine = stateMachine; 39 | this.raftParameters = raftParameters; 40 | this.rpcClientFactory = rpcClientFactory; 41 | this.rpcListener = rpcListener; 42 | this.loggerFactory = logFactory; 43 | this.scheduledExecutor = scheduledExecutor; 44 | if(this.scheduledExecutor == null){ 45 | this.scheduledExecutor = new ScheduledThreadPoolExecutor(Runtime.getRuntime().availableProcessors()); 46 | } 47 | 48 | if(this.raftParameters == null){ 49 | this.raftParameters = new RaftParameters() 50 | .withElectionTimeoutUpper(300) 51 | .withElectionTimeoutLower(150) 52 | .withHeartbeatInterval(75) 53 | .withRpcFailureBackoff(25) 54 | .withMaximumAppendingSize(100) 55 | .withLogSyncBatchSize(1000) 56 | .withLogSyncStoppingGap(100) 57 | .withSnapshotEnabled(0) 58 | .withSyncSnapshotBlockSize(0); 59 | } 60 | } 61 | 62 | public ServerStateManager getServerStateManager() { 63 | return serverStateManager; 64 | } 65 | 66 | public RpcListener getRpcListener() { 67 | return rpcListener; 68 | } 69 | 70 | public LoggerFactory getLoggerFactory() { 71 | return loggerFactory; 72 | } 73 | 74 | public RpcClientFactory getRpcClientFactory() { 75 | return rpcClientFactory; 76 | } 77 | 78 | public StateMachine getStateMachine() { 79 | return stateMachine; 80 | } 81 | 82 | public RaftParameters getRaftParameters() { 83 | return raftParameters; 84 | } 85 | 86 | public ScheduledThreadPoolExecutor getScheduledExecutor(){ 87 | return this.scheduledExecutor; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/RaftMessage.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | public class RaftMessage { 21 | 22 | private RaftMessageType messageType; 23 | private int source; 24 | private int destination; 25 | private long term; 26 | 27 | public RaftMessageType getMessageType() { 28 | return messageType; 29 | } 30 | 31 | public void setMessageType(RaftMessageType messageType) { 32 | this.messageType = messageType; 33 | } 34 | 35 | public int getSource() { 36 | return source; 37 | } 38 | 39 | public void setSource(int source) { 40 | this.source = source; 41 | } 42 | 43 | public int getDestination() { 44 | return destination; 45 | } 46 | 47 | public void setDestination(int destination) { 48 | this.destination = destination; 49 | } 50 | 51 | public long getTerm() { 52 | return term; 53 | } 54 | 55 | public void setTerm(long term) { 56 | this.term = term; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/RaftMessageHandler.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | public interface RaftMessageHandler { 21 | 22 | public RaftResponseMessage processRequest(RaftRequestMessage request); 23 | } 24 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/RaftMessageSender.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | import java.util.concurrent.CompletableFuture; 21 | 22 | public interface RaftMessageSender { 23 | 24 | /** 25 | * Add a new server to the cluster 26 | * @param server new member of cluster 27 | * @return true if request is accepted, or false if no leader, rpc fails or leader declines 28 | */ 29 | CompletableFuture addServer(ClusterServer server); 30 | 31 | /** 32 | * Remove a server from cluster, the server will step down when the removal is confirmed 33 | * @param serverId the id for the server to be removed 34 | * @return true if request is accepted or false if no leader, rpc fails or leader declines 35 | */ 36 | CompletableFuture removeServer(int serverId); 37 | 38 | /** 39 | * Append multiple application logs to log store 40 | * @param values the application log entries 41 | * @return true if request is accepted or false if no leader, rpc fails or leader declines 42 | */ 43 | CompletableFuture appendEntries(byte[][] values); 44 | } 45 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/RaftMessageType.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | public enum RaftMessageType { 21 | 22 | RequestVoteRequest { 23 | @Override 24 | public String toString() { 25 | return "RequestVoteRequest"; 26 | } 27 | 28 | @Override 29 | public byte toByte() { 30 | return (byte) 1; 31 | } 32 | }, 33 | RequestVoteResponse { 34 | @Override 35 | public String toString() { 36 | return "RequestVoteResponse"; 37 | } 38 | 39 | @Override 40 | public byte toByte() { 41 | return (byte) 2; 42 | } 43 | }, 44 | AppendEntriesRequest { 45 | @Override 46 | public String toString() { 47 | return "AppendEntriesRequest"; 48 | } 49 | 50 | @Override 51 | public byte toByte() { 52 | return (byte) 3; 53 | } 54 | }, 55 | AppendEntriesResponse { 56 | @Override 57 | public String toString() { 58 | return "AppendEntriesResponse"; 59 | } 60 | 61 | @Override 62 | public byte toByte() { 63 | return (byte) 4; 64 | } 65 | }, 66 | ClientRequest { 67 | @Override 68 | public String toString() { 69 | return "ClientRequest"; 70 | } 71 | 72 | @Override 73 | public byte toByte() { 74 | return (byte) 5; 75 | } 76 | }, 77 | AddServerRequest { 78 | @Override 79 | public String toString() { 80 | return "AddServerRequest"; 81 | } 82 | 83 | @Override 84 | public byte toByte() { 85 | return (byte) 6; 86 | } 87 | }, 88 | AddServerResponse { 89 | @Override 90 | public String toString() { 91 | return "AddServerResponse"; 92 | } 93 | 94 | @Override 95 | public byte toByte() { 96 | return (byte) 7; 97 | } 98 | }, 99 | RemoveServerRequest { 100 | @Override 101 | public String toString(){ 102 | return "RemoveServerRequest"; 103 | } 104 | 105 | @Override 106 | public byte toByte(){ 107 | return (byte)8; 108 | } 109 | }, 110 | RemoveServerResponse { 111 | @Override 112 | public String toString(){ 113 | return "RemoveServerResponse"; 114 | } 115 | 116 | @Override 117 | public byte toByte(){ 118 | return (byte)9; 119 | } 120 | }, 121 | SyncLogRequest { 122 | @Override 123 | public String toString(){ 124 | return "SyncLogRequest"; 125 | } 126 | 127 | @Override 128 | public byte toByte(){ 129 | return (byte)10; 130 | } 131 | }, 132 | SyncLogResponse { 133 | @Override 134 | public String toString(){ 135 | return "SyncLogResponse"; 136 | } 137 | 138 | @Override 139 | public byte toByte(){ 140 | return (byte)11; 141 | } 142 | }, 143 | JoinClusterRequest { 144 | @Override 145 | public String toString(){ 146 | return "JoinClusterRequest"; 147 | } 148 | 149 | @Override 150 | public byte toByte(){ 151 | return (byte)12; 152 | } 153 | }, 154 | JoinClusterResponse { 155 | @Override 156 | public String toString(){ 157 | return "JoinClusterResponse"; 158 | } 159 | 160 | @Override 161 | public byte toByte(){ 162 | return (byte)13; 163 | } 164 | }, 165 | LeaveClusterRequest { 166 | @Override 167 | public String toString(){ 168 | return "LeaveClusterRequest"; 169 | } 170 | 171 | @Override 172 | public byte toByte(){ 173 | return (byte)14; 174 | } 175 | }, 176 | LeaveClusterResponse { 177 | @Override 178 | public String toString(){ 179 | return "LeaveClusterResponse"; 180 | } 181 | 182 | @Override 183 | public byte toByte(){ 184 | return (byte)15; 185 | } 186 | }, 187 | InstallSnapshotRequest { 188 | @Override 189 | public String toString(){ 190 | return "InstallSnapshotRequest"; 191 | } 192 | 193 | @Override 194 | public byte toByte(){ 195 | return (byte)16; 196 | } 197 | }, 198 | InstallSnapshotResponse { 199 | @Override 200 | public String toString(){ 201 | return "InstallSnapshotResponse"; 202 | } 203 | 204 | @Override 205 | public byte toByte(){ 206 | return (byte)17; 207 | } 208 | }; 209 | 210 | public abstract byte toByte(); 211 | 212 | public static RaftMessageType fromByte(byte value) { 213 | switch (value) { 214 | case 1: 215 | return RequestVoteRequest; 216 | case 2: 217 | return RequestVoteResponse; 218 | case 3: 219 | return AppendEntriesRequest; 220 | case 4: 221 | return AppendEntriesResponse; 222 | case 5: 223 | return ClientRequest; 224 | case 6: 225 | return AddServerRequest; 226 | case 7: 227 | return AddServerResponse; 228 | case 8: 229 | return RemoveServerRequest; 230 | case 9: 231 | return RemoveServerResponse; 232 | case 10: 233 | return SyncLogRequest; 234 | case 11: 235 | return SyncLogResponse; 236 | case 12: 237 | return JoinClusterRequest; 238 | case 13: 239 | return JoinClusterResponse; 240 | case 14: 241 | return LeaveClusterRequest; 242 | case 15: 243 | return LeaveClusterResponse; 244 | case 16: 245 | return InstallSnapshotRequest; 246 | case 17: 247 | return InstallSnapshotResponse; 248 | } 249 | 250 | throw new IllegalArgumentException("the value for the message type is not defined"); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/RaftParameters.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | public class RaftParameters { 21 | 22 | private int electionTimeoutUpperBound; 23 | private int electionTimeoutLowerBound; 24 | private int heartbeatInterval; 25 | private int rpcFailureBackoff; 26 | private int logSyncBatchSize; 27 | private int logSyncStopGap; 28 | private int snapshotDistance; 29 | private int snapshotBlockSize; 30 | private int maxAppendingSize; 31 | 32 | /** 33 | * The tcp block size for syncing the snapshots 34 | * @param size size of sync block 35 | * @return self 36 | */ 37 | public RaftParameters withSyncSnapshotBlockSize(int size){ 38 | this.snapshotBlockSize = size; 39 | return this; 40 | } 41 | 42 | /** 43 | * Enable log compact and snapshot with the commit distance 44 | * @param distance log distance to compact between two snapshots 45 | * @return self 46 | */ 47 | public RaftParameters withSnapshotEnabled(int distance){ 48 | this.snapshotDistance = distance; 49 | return this; 50 | } 51 | 52 | /** 53 | * For new member that just joined the cluster, we will use log sync to ask it to catch up, 54 | * and this parameter is to tell when to stop using log sync but appendEntries for the new server 55 | * when leaderCommitIndex - indexCaughtUp < logSyncStopGap, then appendEntries will be used 56 | * @param logSyncStopGap the log gap to stop log pack-and-sync feature 57 | * @return self 58 | */ 59 | public RaftParameters withLogSyncStoppingGap(int logSyncStopGap){ 60 | this.logSyncStopGap = logSyncStopGap; 61 | return this; 62 | } 63 | 64 | /** 65 | * For new member that just joined the cluster, we will use log sync to ask it to catch up, 66 | * and this parameter is to specify how many log entries to pack for each sync request 67 | * @param logSyncBatchSize the batch size fo pack-and-sync feature 68 | * @return self 69 | */ 70 | public RaftParameters withLogSyncBatchSize(int logSyncBatchSize){ 71 | this.logSyncBatchSize = logSyncBatchSize; 72 | return this; 73 | } 74 | 75 | /** 76 | * The maximum log entries could be attached to an appendEntries call 77 | * @param maxAppendingSize size limit for appendEntries call 78 | * @return self 79 | */ 80 | public RaftParameters withMaximumAppendingSize(int maxAppendingSize){ 81 | this.maxAppendingSize = maxAppendingSize; 82 | return this; 83 | } 84 | 85 | /** 86 | * Election timeout upper bound in milliseconds 87 | * @param electionTimeoutUpper election timeout upper value 88 | * @return self 89 | */ 90 | public RaftParameters withElectionTimeoutUpper(int electionTimeoutUpper){ 91 | this.electionTimeoutUpperBound = electionTimeoutUpper; 92 | return this; 93 | } 94 | 95 | /** 96 | * Election timeout lower bound in milliseconds 97 | * @param electionTimeoutLower election timeout lower value 98 | * @return self 99 | */ 100 | public RaftParameters withElectionTimeoutLower(int electionTimeoutLower){ 101 | this.electionTimeoutLowerBound = electionTimeoutLower; 102 | return this; 103 | } 104 | 105 | /** 106 | * heartbeat interval in milliseconds 107 | * @param heartbeatInterval heart beat interval 108 | * @return self 109 | */ 110 | public RaftParameters withHeartbeatInterval(int heartbeatInterval){ 111 | this.heartbeatInterval = heartbeatInterval; 112 | return this; 113 | } 114 | 115 | /** 116 | * Rpc failure backoff in milliseconds 117 | * @param rpcFailureBackoff rpc failure back off 118 | * @return self 119 | */ 120 | public RaftParameters withRpcFailureBackoff(int rpcFailureBackoff){ 121 | this.rpcFailureBackoff = rpcFailureBackoff; 122 | return this; 123 | } 124 | 125 | /** 126 | * Upper value for election timeout 127 | * @return upper of election timeout in milliseconds 128 | */ 129 | public int getElectionTimeoutUpperBound() { 130 | return electionTimeoutUpperBound; 131 | } 132 | 133 | /** 134 | * Lower value for election timeout 135 | * @return lower of election timeout in milliseconds 136 | */ 137 | public int getElectionTimeoutLowerBound() { 138 | return electionTimeoutLowerBound; 139 | } 140 | 141 | /** 142 | * Heartbeat interval for each peer 143 | * @return heartbeat interval in milliseconds 144 | */ 145 | public int getHeartbeatInterval() { 146 | return heartbeatInterval; 147 | } 148 | 149 | /** 150 | * Rpc backoff for peers that failed to be connected 151 | * @return rpc backoff in milliseconds 152 | */ 153 | public int getRpcFailureBackoff() { 154 | return rpcFailureBackoff; 155 | } 156 | 157 | /** 158 | * The maximum heartbeat interval, any value beyond this may lead to election timeout for a peer before receiving a heartbeat 159 | * @return maximum heartbeat interval (including rpc backoff) in milliseconds 160 | */ 161 | public int getMaxHeartbeatInterval(){ 162 | return Math.max(this.heartbeatInterval, this.electionTimeoutLowerBound - this.heartbeatInterval / 2); 163 | } 164 | 165 | /** 166 | * The batch size for each ReplicateLogRequest message 167 | * @return batch size in bytes 168 | */ 169 | public int getLogSyncBatchSize() { 170 | return logSyncBatchSize; 171 | } 172 | 173 | /** 174 | * the max gap allowed for log sync, if the gap between the client and leader is less than this value, 175 | * the ReplicateLogRequest will be stopped 176 | * @return maximum gap allowed in bytes for log sync 177 | */ 178 | public int getLogSyncStopGap() { 179 | return logSyncStopGap; 180 | } 181 | 182 | /** 183 | * The commit distances for snapshots, zero means don't take any snapshots 184 | * @return commit distances for log store 185 | */ 186 | public int getSnapshotDistance(){ 187 | return this.snapshotDistance; 188 | } 189 | 190 | /** 191 | * The block size to sync while syncing snapshots to peers 192 | * @return block size in bytes 193 | */ 194 | public int getSnapshotBlockSize() { 195 | return snapshotBlockSize; 196 | } 197 | 198 | /** 199 | * The maximum log entries in an appendEntries request 200 | * @return maximum log entries 201 | */ 202 | public int getMaximumAppendingSize(){ 203 | return this.maxAppendingSize; 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/RaftRequestMessage.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | public class RaftRequestMessage extends RaftMessage { 21 | 22 | private long lastLogTerm; 23 | private long lastLogIndex; 24 | private long commitIndex; 25 | private LogEntry[] logEntries; 26 | 27 | public long getLastLogTerm() { 28 | return lastLogTerm; 29 | } 30 | 31 | public void setLastLogTerm(long lastLogTerm) { 32 | this.lastLogTerm = lastLogTerm; 33 | } 34 | 35 | public long getLastLogIndex() { 36 | return lastLogIndex; 37 | } 38 | 39 | public void setLastLogIndex(long lastLogIndex) { 40 | this.lastLogIndex = lastLogIndex; 41 | } 42 | 43 | public long getCommitIndex() { 44 | return commitIndex; 45 | } 46 | 47 | public void setCommitIndex(long commitIndex) { 48 | this.commitIndex = commitIndex; 49 | } 50 | 51 | public LogEntry[] getLogEntries() { 52 | return logEntries; 53 | } 54 | 55 | public void setLogEntries(LogEntry[] logEntries) { 56 | this.logEntries = logEntries; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/RaftResponseMessage.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | public class RaftResponseMessage extends RaftMessage { 21 | 22 | private long nextIndex; 23 | private boolean accepted; 24 | 25 | public long getNextIndex() { 26 | return nextIndex; 27 | } 28 | 29 | public void setNextIndex(long nextIndex) { 30 | this.nextIndex = nextIndex; 31 | } 32 | 33 | public boolean isAccepted() { 34 | return accepted; 35 | } 36 | 37 | public void setAccepted(boolean accepted) { 38 | this.accepted = accepted; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/RpcClient.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | import java.util.concurrent.CompletableFuture; 21 | 22 | public interface RpcClient { 23 | 24 | /** 25 | * Sends a RaftRequestMessage to peer and read a response from peer 26 | * this will not be called concurrently 27 | * @param request Raft rpc request message 28 | * @return Raft rpc response 29 | */ 30 | public CompletableFuture send(RaftRequestMessage request); 31 | } 32 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/RpcClientFactory.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | public interface RpcClientFactory { 21 | 22 | /** 23 | * Creates a RpcClient for the given endpoint 24 | * @param endpoint endpoint for the server 25 | * @return an instance of RpcClient 26 | */ 27 | public RpcClient createRpcClient(String endpoint); 28 | } 29 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/RpcException.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | public class RpcException extends RuntimeException { 21 | /** 22 | * 23 | */ 24 | private static final long serialVersionUID = 7441647103555107828L; 25 | 26 | private RaftRequestMessage request; 27 | 28 | public RpcException(Throwable realException, RaftRequestMessage request){ 29 | super(realException.getMessage(), realException); 30 | this.request = request; 31 | } 32 | 33 | public RaftRequestMessage getRequest(){ 34 | return this.request; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/RpcListener.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | public interface RpcListener { 21 | 22 | /** 23 | * Starts listening and handle all incoming messages with messageHandler 24 | * @param messageHandler the message handler to handle all incoming requests 25 | */ 26 | public void startListening(RaftMessageHandler messageHandler); 27 | 28 | /** 29 | * Stop listening and clean up 30 | */ 31 | public void stop(); 32 | } 33 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/SequentialLogStore.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | public interface SequentialLogStore { 21 | 22 | /** 23 | * The first available index of the store, starts with 1 24 | * @return value >= 1 25 | */ 26 | public long getFirstAvailableIndex(); 27 | 28 | /** 29 | * The start index of the log store, at the very beginning, it must be 1 30 | * however, after some compact actions, this could be anything greater or equals to one 31 | * @return start index of the log store 32 | */ 33 | public long getStartIndex(); 34 | 35 | /** 36 | * The last log entry in store 37 | * @return a dummy constant entry with value set to null and term set to zero if no log entry in store 38 | */ 39 | public LogEntry getLastLogEntry(); 40 | 41 | /** 42 | * Appends a log entry to store 43 | * @param logEntry log entry to append 44 | * @return the last appended log index 45 | */ 46 | public long append(LogEntry logEntry); 47 | 48 | /** 49 | * Over writes a log entry at index of {@code index} 50 | * @param index a value < {@code this.getFirstAvailableIndex()}, and starts from 1 51 | * @param logEntry log entry to write 52 | */ 53 | public void writeAt(long index, LogEntry logEntry); 54 | 55 | /** 56 | * Get log entries with index between {@code start} and {@code end} 57 | * @param start the start index of log entries 58 | * @param end the end index of log entries (exclusive) 59 | * @return the log entries between [start, end) 60 | */ 61 | public LogEntry[] getLogEntries(long start, long end); 62 | 63 | /** 64 | * Gets the log entry at the specified index 65 | * @param index starts from 1 66 | * @return the log entry or null if index >= {@code this.getFirstAvailableIndex()} 67 | */ 68 | public LogEntry getLogEntryAt(long index); 69 | 70 | /** 71 | * Pack {@code itemsToPack} log items starts from {@code index} 72 | * @param index index of the log store to start 73 | * @param itemsToPack items to pack 74 | * @return log pack 75 | */ 76 | public byte[] packLog(long index, int itemsToPack); 77 | 78 | /** 79 | * Apply the log pack to current log store, starting from index 80 | * @param index the log index that start applying the logPack, index starts from 1 81 | * @param logPack log pack to apply 82 | */ 83 | public void applyLogPack(long index, byte[] logPack); 84 | 85 | /** 86 | * Compact the log store by removing all log entries including the log at the lastLogIndex 87 | * @param lastLogIndex pack until the last log index 88 | * @return compact successfully or not 89 | */ 90 | public boolean compact(long lastLogIndex); 91 | } 92 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/ServerRole.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | public enum ServerRole { 21 | 22 | Follower, 23 | Candidate, 24 | Leader 25 | } 26 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/ServerState.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | public class ServerState { 21 | 22 | private long term; 23 | private long commitIndex; 24 | private int votedFor; 25 | 26 | public long getTerm() { 27 | return term; 28 | } 29 | 30 | public void setTerm(long term) { 31 | this.term = term; 32 | } 33 | 34 | public int getVotedFor() { 35 | return votedFor; 36 | } 37 | 38 | public void setVotedFor(int votedFor) { 39 | this.votedFor = votedFor; 40 | } 41 | 42 | public void increaseTerm(){ 43 | this.term += 1; 44 | } 45 | 46 | public long getCommitIndex() { 47 | return commitIndex; 48 | } 49 | 50 | public void setCommitIndex(long commitIndex) { 51 | if(commitIndex > this.commitIndex){ 52 | this.commitIndex = commitIndex; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/ServerStateManager.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | public interface ServerStateManager { 21 | 22 | /** 23 | * Load cluster configuration for this server 24 | * @return the cluster configuration, never be null 25 | */ 26 | public ClusterConfiguration loadClusterConfiguration(); 27 | 28 | /** 29 | * Save the cluster configuration 30 | * @param configuration cluster configuration to save 31 | */ 32 | public void saveClusterConfiguration(ClusterConfiguration configuration); 33 | 34 | /** 35 | * Save the server state 36 | * @param serverState server state to persist 37 | */ 38 | public void persistState(ServerState serverState); 39 | 40 | /** 41 | * Load server state 42 | * @return the server state, never be null 43 | */ 44 | public ServerState readState(); 45 | 46 | /** 47 | * Load the log store for current server 48 | * @return the log store, never be null 49 | */ 50 | public SequentialLogStore loadLogStore(); 51 | 52 | /** 53 | * Get current server id 54 | * @return server id for this server 55 | */ 56 | public int getServerId(); 57 | } 58 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/Snapshot.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | public class Snapshot { 21 | 22 | private long lastLogIndex; 23 | private long lastLogTerm; 24 | private long size; 25 | private ClusterConfiguration lastConfig; 26 | 27 | public Snapshot(long lastLogIndex, long lastLogTerm, ClusterConfiguration lastConfig){ 28 | this(lastLogIndex, lastLogTerm, lastConfig, 0); 29 | } 30 | 31 | public Snapshot(long lastLogIndex, long lastLogTerm, ClusterConfiguration lastConfig, long size){ 32 | this.lastConfig = lastConfig; 33 | this.lastLogIndex = lastLogIndex; 34 | this.lastLogTerm = lastLogTerm; 35 | this.size = size; 36 | } 37 | 38 | public long getLastLogIndex() { 39 | return lastLogIndex; 40 | } 41 | 42 | public long getLastLogTerm() { 43 | return lastLogTerm; 44 | } 45 | 46 | public ClusterConfiguration getLastConfig() { 47 | return lastConfig; 48 | } 49 | 50 | public long getSize(){ 51 | return this.size; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/SnapshotSyncContext.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | public class SnapshotSyncContext { 21 | 22 | private Snapshot snapshot; 23 | private long offset; 24 | 25 | public SnapshotSyncContext(Snapshot snapshot){ 26 | this.snapshot = snapshot; 27 | this.offset = 0L; 28 | } 29 | 30 | public Snapshot getSnapshot(){ 31 | return this.snapshot; 32 | } 33 | 34 | public void setOffset(long offset){ 35 | this.offset = offset; 36 | } 37 | 38 | public long getOffset(){ 39 | return this.offset; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/SnapshotSyncRequest.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | import java.nio.ByteBuffer; 21 | 22 | public class SnapshotSyncRequest { 23 | 24 | private Snapshot snapshot; 25 | private long offset; 26 | private byte[] data; 27 | private boolean done; 28 | 29 | public SnapshotSyncRequest(Snapshot snapshot, long offset, byte[] data, boolean done) { 30 | this.snapshot = snapshot; 31 | this.offset = offset; 32 | this.data = data; 33 | this.done = done; 34 | } 35 | 36 | public Snapshot getSnapshot() { 37 | return snapshot; 38 | } 39 | 40 | public long getOffset() { 41 | return offset; 42 | } 43 | 44 | public byte[] getData() { 45 | return data; 46 | } 47 | 48 | public boolean isDone() { 49 | return done; 50 | } 51 | 52 | public byte[] toBytes(){ 53 | byte[] configData = this.snapshot.getLastConfig().toBytes(); 54 | int size = Long.BYTES * 3 + configData.length + Integer.BYTES * 2 + data.length + 1; 55 | ByteBuffer buffer = ByteBuffer.allocate(size); 56 | buffer.putLong(snapshot.getLastLogIndex()); 57 | buffer.putLong(snapshot.getLastLogTerm()); 58 | buffer.putInt(configData.length); 59 | buffer.put(configData); 60 | buffer.putLong(this.offset); 61 | buffer.putInt(this.data.length); 62 | buffer.put(this.data); 63 | buffer.put(this.done ? (byte)1 : (byte)0); 64 | return buffer.array(); 65 | } 66 | 67 | public static SnapshotSyncRequest fromBytes(byte[] bytes){ 68 | ByteBuffer buffer = ByteBuffer.wrap(bytes); 69 | long lastLogIndex = buffer.getLong(); 70 | long lastLogTerm = buffer.getLong(); 71 | int configSize = buffer.getInt(); 72 | ClusterConfiguration config = ClusterConfiguration.fromBytes(ByteBuffer.wrap(bytes, buffer.position(), configSize)); 73 | buffer.position(buffer.position() + configSize); 74 | long offset = buffer.getLong(); 75 | int dataSize = buffer.getInt(); 76 | byte[] data = new byte[dataSize]; 77 | buffer.get(data); 78 | boolean done = buffer.get() == 1; 79 | return new SnapshotSyncRequest(new Snapshot(lastLogIndex, lastLogTerm, config), offset, data, done); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /core/src/main/java/net/data/technology/jraft/StateMachine.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | import java.util.concurrent.CompletableFuture; 21 | 22 | public interface StateMachine { 23 | 24 | /** 25 | * Starts the state machine, called by RaftConsensus, RaftConsensus will pass an instance of 26 | * RaftMessageSender for the state machine to send logs to cluster, so that all state machines 27 | * in the same cluster could be in synced 28 | * @param raftMessageSender rpc message sender 29 | */ 30 | public void start(RaftMessageSender raftMessageSender); 31 | 32 | /** 33 | * Commit the log data at the {@code logIndex} 34 | * @param logIndex the log index in the logStore 35 | * @param data application data to commit 36 | */ 37 | public void commit(long logIndex, byte[] data); 38 | 39 | /** 40 | * Rollback a preCommit item at index {@code logIndex} 41 | * @param logIndex log index to be rolled back 42 | * @param data application data to rollback 43 | */ 44 | public void rollback(long logIndex, byte[] data); 45 | 46 | /** 47 | * PreCommit a log entry at log index {@code logIndex} 48 | * @param logIndex the log index to commit 49 | * @param data application data for pre-commit 50 | */ 51 | public void preCommit(long logIndex, byte[] data); 52 | 53 | /** 54 | * Save data for the snapshot 55 | * @param snapshot the snapshot information 56 | * @param offset offset of the data in the whole snapshot 57 | * @param data part of snapshot data 58 | */ 59 | public void saveSnapshotData(Snapshot snapshot, long offset, byte[] data); 60 | 61 | /** 62 | * Apply a snapshot to current state machine 63 | * @param snapshot the snapshot to be applied 64 | * @return true if successfully applied, otherwise false 65 | */ 66 | public boolean applySnapshot(Snapshot snapshot); 67 | 68 | /** 69 | * Read snapshot data at the specified offset to buffer and return bytes read 70 | * @param snapshot the snapshot info 71 | * @param offset the offset of the snapshot data 72 | * @param buffer the buffer to be filled 73 | * @return bytes read 74 | */ 75 | public int readSnapshotData(Snapshot snapshot, long offset, byte[] buffer); 76 | 77 | /** 78 | * Read the last snapshot information 79 | * @return last snapshot information in the state machine or null if none 80 | */ 81 | public Snapshot getLastSnapshot(); 82 | 83 | /** 84 | * Create a snapshot data based on the snapshot information asynchronously 85 | * set the future to true if snapshot is successfully created, otherwise, 86 | * set it to false 87 | * @param snapshot the snapshot info 88 | * @return true if snapshot is created successfully, otherwise false 89 | */ 90 | public CompletableFuture createSnapshot(Snapshot snapshot); 91 | 92 | /** 93 | * Save the state of state machine to ensure the state machine is in a good state, then exit the system 94 | * this MUST exits the system to protect the safety of the algorithm 95 | * @param code 0 indicates the system is gracefully shutdown, -1 indicates there are some errors which cannot be recovered 96 | */ 97 | public void exit(int code); 98 | } 99 | -------------------------------------------------------------------------------- /core/src/test/java/net/data/technology/jraft/tests/ClusterConfigurationTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.tests; 19 | 20 | import static org.junit.Assert.*; 21 | 22 | import java.util.Calendar; 23 | import java.util.Random; 24 | 25 | import org.junit.Test; 26 | 27 | import net.data.technology.jraft.ClusterConfiguration; 28 | import net.data.technology.jraft.ClusterServer; 29 | 30 | public class ClusterConfigurationTests { 31 | 32 | @Test 33 | public void testSerialization() { 34 | ClusterConfiguration config = new ClusterConfiguration(); 35 | Random random = new Random(Calendar.getInstance().getTimeInMillis()); 36 | config.setLastLogIndex(random.nextLong()); 37 | config.setLogIndex(random.nextLong()); 38 | int servers = random.nextInt(10) + 1; 39 | for(int i = 0; i < servers; ++i){ 40 | ClusterServer server = new ClusterServer(); 41 | server.setId(random.nextInt()); 42 | server.setEndpoint(String.format("Server %d", (i + 1))); 43 | config.getServers().add(server); 44 | } 45 | 46 | byte[] data = config.toBytes(); 47 | ClusterConfiguration config1 = ClusterConfiguration.fromBytes(data); 48 | assertEquals(config.getLastLogIndex(), config1.getLastLogIndex()); 49 | assertEquals(config.getLogIndex(), config1.getLogIndex()); 50 | assertEquals(config.getServers().size(), config1.getServers().size()); 51 | for(int i = 0; i < config.getServers().size(); ++i){ 52 | ClusterServer s1 = config.getServers().get(i); 53 | ClusterServer s2 = config.getServers().get(i); 54 | assertEquals(s1.getId(), s2.getId()); 55 | assertEquals(s1.getEndpoint(), s2.getEndpoint()); 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /core/src/test/java/net/data/technology/jraft/tests/SnapshotSyncRequestTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.tests; 19 | 20 | import static org.junit.Assert.*; 21 | 22 | import java.util.Calendar; 23 | import java.util.Random; 24 | 25 | import org.junit.Test; 26 | 27 | import net.data.technology.jraft.ClusterConfiguration; 28 | import net.data.technology.jraft.ClusterServer; 29 | import net.data.technology.jraft.Snapshot; 30 | import net.data.technology.jraft.SnapshotSyncRequest; 31 | 32 | public class SnapshotSyncRequestTests { 33 | 34 | @Test 35 | public void testSerialization() { 36 | ClusterConfiguration config = new ClusterConfiguration(); 37 | Random random = new Random(Calendar.getInstance().getTimeInMillis()); 38 | config.setLastLogIndex(random.nextLong()); 39 | config.setLogIndex(random.nextLong()); 40 | int servers = random.nextInt(10) + 1; 41 | for(int i = 0; i < servers; ++i){ 42 | ClusterServer server = new ClusterServer(); 43 | server.setId(random.nextInt()); 44 | server.setEndpoint(String.format("Server %d", (i + 1))); 45 | config.getServers().add(server); 46 | } 47 | 48 | Snapshot snapshot = new Snapshot(random.nextLong(), random.nextLong(), config); 49 | byte[] snapshotData = new byte[random.nextInt(200) + 1]; 50 | random.nextBytes(snapshotData); 51 | SnapshotSyncRequest request = new SnapshotSyncRequest(snapshot, random.nextLong(), snapshotData, random.nextBoolean()); 52 | byte[] data = request.toBytes(); 53 | SnapshotSyncRequest request1 = SnapshotSyncRequest.fromBytes(data); 54 | assertEquals(request.getOffset(), request1.getOffset()); 55 | assertEquals(request.isDone(), request1.isDone()); 56 | Snapshot snapshot1 = request1.getSnapshot(); 57 | assertEquals(snapshot.getLastLogIndex(), snapshot1.getLastLogIndex()); 58 | assertEquals(snapshot.getLastLogTerm(), snapshot1.getLastLogTerm()); 59 | ClusterConfiguration config1 = snapshot1.getLastConfig(); 60 | assertEquals(config.getLastLogIndex(), config1.getLastLogIndex()); 61 | assertEquals(config.getLogIndex(), config1.getLogIndex()); 62 | assertEquals(config.getServers().size(), config1.getServers().size()); 63 | for(int i = 0; i < config.getServers().size(); ++i){ 64 | ClusterServer s1 = config.getServers().get(i); 65 | ClusterServer s2 = config.getServers().get(i); 66 | assertEquals(s1.getId(), s2.getId()); 67 | assertEquals(s1.getEndpoint(), s2.getEndpoint()); 68 | } 69 | 70 | byte[] snapshotData1 = request1.getData(); 71 | assertEquals(snapshotData.length, snapshotData1.length); 72 | for(int i = 0; i < snapshotData.length; ++i){ 73 | assertEquals(snapshotData[i], snapshotData1[i]); 74 | } 75 | } 76 | 77 | @Test 78 | public void testSerializationWithZeroData() { 79 | ClusterConfiguration config = new ClusterConfiguration(); 80 | Random random = new Random(Calendar.getInstance().getTimeInMillis()); 81 | config.setLastLogIndex(random.nextLong()); 82 | config.setLogIndex(random.nextLong()); 83 | int servers = random.nextInt(10) + 1; 84 | for(int i = 0; i < servers; ++i){ 85 | ClusterServer server = new ClusterServer(); 86 | server.setId(random.nextInt()); 87 | server.setEndpoint(String.format("Server %d", (i + 1))); 88 | config.getServers().add(server); 89 | } 90 | 91 | Snapshot snapshot = new Snapshot(random.nextLong(), random.nextLong(), config); 92 | byte[] snapshotData = new byte[0]; 93 | SnapshotSyncRequest request = new SnapshotSyncRequest(snapshot, random.nextLong(), snapshotData, random.nextBoolean()); 94 | byte[] data = request.toBytes(); 95 | SnapshotSyncRequest request1 = SnapshotSyncRequest.fromBytes(data); 96 | assertEquals(request.getOffset(), request1.getOffset()); 97 | assertEquals(request.isDone(), request1.isDone()); 98 | Snapshot snapshot1 = request1.getSnapshot(); 99 | assertEquals(snapshot.getLastLogIndex(), snapshot1.getLastLogIndex()); 100 | assertEquals(snapshot.getLastLogTerm(), snapshot1.getLastLogTerm()); 101 | ClusterConfiguration config1 = snapshot1.getLastConfig(); 102 | assertEquals(config.getLastLogIndex(), config1.getLastLogIndex()); 103 | assertEquals(config.getLogIndex(), config1.getLogIndex()); 104 | assertEquals(config.getServers().size(), config1.getServers().size()); 105 | for(int i = 0; i < config.getServers().size(); ++i){ 106 | ClusterServer s1 = config.getServers().get(i); 107 | ClusterServer s2 = config.getServers().get(i); 108 | assertEquals(s1.getId(), s2.getId()); 109 | assertEquals(s1.getEndpoint(), s2.getEndpoint()); 110 | } 111 | 112 | byte[] snapshotData1 = request1.getData(); 113 | assertEquals(snapshotData.length, snapshotData1.length); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /dmprinter/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | 6 | net.data-technology.jraft 7 | jraft-root 8 | 1.0 9 | 10 | 11 | net.data-technology.jraft 12 | dmprinter 13 | 1.0.0 14 | jar 15 | 16 | dmprinter 17 | http://maven.apache.org 18 | 19 | 20 | UTF-8 21 | 22 | 23 | 24 | 25 | junit 26 | junit 27 | test 28 | 29 | 30 | net.data-technology.jraft 31 | jraft-core 32 | 1.0.0 33 | 34 | 35 | net.data-technology.jraft 36 | jraft-exts 37 | 1.0.0 38 | 39 | 40 | com.google.code.gson 41 | gson 42 | 43 | 44 | 45 | 46 | 47 | org.apache.maven.plugins 48 | maven-jar-plugin 49 | 50 | 51 | 52 | net.data.technology.jraft.App 53 | 54 | 55 | 56 | 57 | 58 | org.apache.maven.plugins 59 | maven-compiler-plugin 60 | 3.5.1 61 | 62 | 1.8 63 | 1.8 64 | 65 | 66 | 67 | org.apache.maven.plugins 68 | maven-dependency-plugin 69 | 70 | 71 | copy-artifact 72 | package 73 | 74 | copy-dependencies 75 | 76 | 77 | target/ 78 | 79 | 80 | 81 | 82 | 83 | org.apache.maven.plugins 84 | maven-antrun-plugin 85 | 1.8 86 | 87 | 88 | run-dmprinter 89 | test 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | run 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /dmprinter/src/main/java/jraft.json: -------------------------------------------------------------------------------- 1 | { 2 | "localServerId": 1, 3 | "servers":[ 4 | { 5 | "name": "Server 1", 6 | "id": 1, 7 | "endpoint": "tcp://localhost:9001" 8 | }, 9 | { 10 | "name": "Server 2", 11 | "id": 2, 12 | "endpoint": "tcp://localhost:9002" 13 | }, 14 | { 15 | "name": "Server 3", 16 | "id": 3, 17 | "endpoint": "tcp://localhost:9003" 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /dmprinter/src/main/java/log4j.properties: -------------------------------------------------------------------------------- 1 | # Root logger option 2 | log4j.rootLogger=DEBUG, file 3 | 4 | # Redirect log messages to console 5 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 6 | log4j.appender.stdout.Target=System.out 7 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 8 | log4j.appender.stdout.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}.%t - %m%n 9 | 10 | # Redirect log messages to a log file, support file rolling. 11 | log4j.appender.file=org.apache.log4j.RollingFileAppender 12 | log4j.appender.file.File=raft-debugging.log 13 | log4j.appender.file.MaxFileSize=5MB 14 | log4j.appender.file.MaxBackupIndex=10 15 | log4j.appender.file.layout=org.apache.log4j.PatternLayout 16 | log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}.%t - %m%n -------------------------------------------------------------------------------- /dmprinter/src/main/java/net/data/technology/jraft/App.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | import java.io.BufferedReader; 21 | import java.io.InputStreamReader; 22 | import java.net.URI; 23 | import java.nio.file.Files; 24 | import java.nio.file.Path; 25 | import java.nio.file.Paths; 26 | import java.util.ArrayList; 27 | import java.util.Calendar; 28 | import java.util.LinkedList; 29 | import java.util.List; 30 | import java.util.Random; 31 | import java.util.StringTokenizer; 32 | import java.util.concurrent.CompletableFuture; 33 | import java.util.concurrent.ExecutorService; 34 | import java.util.concurrent.ScheduledThreadPoolExecutor; 35 | 36 | import net.data.technology.jraft.extensions.FileBasedServerStateManager; 37 | import net.data.technology.jraft.extensions.Log4jLoggerFactory; 38 | import net.data.technology.jraft.extensions.RpcTcpClientFactory; 39 | import net.data.technology.jraft.extensions.RpcTcpListener; 40 | 41 | public class App 42 | { 43 | public static void main( String[] args ) throws Exception 44 | { 45 | if(args.length < 2){ 46 | System.out.println("Please specify execution mode and a base directory for this instance."); 47 | return; 48 | } 49 | 50 | if(!"server".equalsIgnoreCase(args[0]) && !"client".equalsIgnoreCase(args[0]) && !"dummy".equalsIgnoreCase(args[0])){ 51 | System.out.println("only client and server modes are supported"); 52 | return; 53 | } 54 | 55 | ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(Runtime.getRuntime().availableProcessors() * 2); 56 | if("dummy".equalsIgnoreCase(args[0])){ 57 | executeInDummyMode(args[1], executor); 58 | return; 59 | } 60 | 61 | Path baseDir = Paths.get(args[1]); 62 | if(!Files.isDirectory(baseDir)){ 63 | System.out.printf("%s does not exist as a directory\n", args[1]); 64 | return; 65 | } 66 | 67 | FileBasedServerStateManager stateManager = new FileBasedServerStateManager(args[1]); 68 | ClusterConfiguration config = stateManager.loadClusterConfiguration(); 69 | 70 | if("client".equalsIgnoreCase(args[0])){ 71 | executeAsClient(config, executor); 72 | return; 73 | } 74 | 75 | // Server mode 76 | int port = 8000; 77 | if(args.length >= 3){ 78 | port = Integer.parseInt(args[2]); 79 | } 80 | URI localEndpoint = new URI(config.getServer(stateManager.getServerId()).getEndpoint()); 81 | RaftParameters raftParameters = new RaftParameters() 82 | .withElectionTimeoutUpper(5000) 83 | .withElectionTimeoutLower(3000) 84 | .withHeartbeatInterval(1500) 85 | .withRpcFailureBackoff(500) 86 | .withMaximumAppendingSize(200) 87 | .withLogSyncBatchSize(5) 88 | .withLogSyncStoppingGap(5) 89 | .withSnapshotEnabled(5000) 90 | .withSyncSnapshotBlockSize(0); 91 | MessagePrinter mp = new MessagePrinter(baseDir, port); 92 | RaftContext context = new RaftContext( 93 | stateManager, 94 | mp, 95 | raftParameters, 96 | new RpcTcpListener(localEndpoint.getPort(), executor), 97 | new Log4jLoggerFactory(), 98 | new RpcTcpClientFactory(executor), 99 | executor); 100 | RaftConsensus.run(context); 101 | System.out.println( "Press Enter to exit." ); 102 | System.in.read(); 103 | mp.stop(); 104 | } 105 | 106 | private static void executeAsClient(ClusterConfiguration configuration, ExecutorService executor) throws Exception{ 107 | RaftClient client = new RaftClient(new RpcTcpClientFactory(executor), configuration, new Log4jLoggerFactory()); 108 | BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 109 | while(true){ 110 | System.out.print("Message:"); 111 | String message = reader.readLine(); 112 | if(message.startsWith("addsrv")){ 113 | StringTokenizer tokenizer = new StringTokenizer(message, ";"); 114 | ArrayList values = new ArrayList(); 115 | while(tokenizer.hasMoreTokens()){ 116 | values.add(tokenizer.nextToken()); 117 | } 118 | 119 | if(values.size() == 3){ 120 | ClusterServer server = new ClusterServer(); 121 | server.setEndpoint(values.get(2)); 122 | server.setId(Integer.parseInt(values.get(1))); 123 | boolean accepted = client.addServer(server).get(); 124 | System.out.println("Accepted: " + String.valueOf(accepted)); 125 | continue; 126 | } 127 | }else if(message.startsWith("fmt:")){ 128 | String format = message.substring(4); 129 | System.out.print("How many?"); 130 | String countValue = reader.readLine(); 131 | int count = Integer.parseInt(countValue.trim()); 132 | for(int i = 1; i <= count; ++i){ 133 | String msg = String.format(format, i); 134 | boolean accepted = client.appendEntries(new byte[][]{ msg.getBytes() }).get(); 135 | System.out.println("Accepted: " + String.valueOf(accepted)); 136 | } 137 | continue; 138 | }else if(message.startsWith("rmsrv:")){ 139 | String text = message.substring(6); 140 | int serverId = Integer.parseInt(text.trim()); 141 | boolean accepted = client.removeServer(serverId).get(); 142 | System.out.println("Accepted: " + String.valueOf(accepted)); 143 | continue; 144 | } 145 | 146 | boolean accepted = client.appendEntries(new byte[][]{ message.getBytes() }).get(); 147 | System.out.println("Accepted: " + String.valueOf(accepted)); 148 | } 149 | } 150 | 151 | /** 152 | * This is used to verify the rpc module's functionality 153 | * @param mode 154 | */ 155 | private static void executeInDummyMode(String mode, ExecutorService executor) throws Exception{ 156 | if("server".equalsIgnoreCase(mode)){ 157 | RpcTcpListener listener = new RpcTcpListener(9001, executor); 158 | listener.startListening(new DummyMessageHandler()); 159 | System.in.read(); 160 | }else{ 161 | RpcClient client = new RpcTcpClientFactory(executor).createRpcClient("tcp://localhost:9001"); 162 | int batchSize = 1000; 163 | List > > list = new LinkedList > >(); 164 | BufferedReader reader = new BufferedReader(new InputStreamReader(System.in)); 165 | System.out.print("ready to start?"); 166 | reader.readLine(); 167 | while(true){ 168 | for(int i = 0; i < batchSize; ++i){ 169 | RaftRequestMessage request = randomRequest(); 170 | request.setSource(i); 171 | CompletableFuture response = client.send(request); 172 | list.add(new Pair>(request, response)); 173 | } 174 | 175 | for(int i = 0; i < batchSize; ++i){ 176 | System.out.printf("Waiting for response %d\n", i); 177 | Pair > item = list.get(i); 178 | RaftRequestMessage request = item.item1; 179 | RaftResponseMessage response = item.item2.get(); 180 | 181 | System.out.println(String.format( 182 | "Response %d: Accepted: %s, Src: %d, Dest: %d, MT: %s, NI: %d, T: %d", 183 | i, 184 | String.valueOf(response.isAccepted()), 185 | response.getSource(), 186 | response.getDestination(), 187 | response.getMessageType(), 188 | response.getNextIndex(), 189 | response.getTerm())); 190 | 191 | if(request.getTerm() != response.getTerm()){ 192 | System.out.printf("fatal: request and response are mismatched, %d v.s. %d @ %s!\n", request.getTerm(), response.getTerm(), item.item2.toString()); 193 | reader.readLine(); 194 | return; 195 | } 196 | } 197 | 198 | System.out.print("Continue?"); 199 | String answer = reader.readLine(); 200 | if(!"yes".equalsIgnoreCase(answer)){ 201 | break; 202 | } 203 | 204 | list.clear(); 205 | } 206 | } 207 | } 208 | 209 | private static Random random = new Random(Calendar.getInstance().getTimeInMillis()); 210 | 211 | private static RaftRequestMessage randomRequest(){ 212 | RaftRequestMessage request = new RaftRequestMessage(); 213 | request.setMessageType(randomMessageType());; 214 | request.setCommitIndex(random.nextLong()); 215 | request.setDestination(random.nextInt()); 216 | request.setLastLogIndex(random.nextLong()); 217 | request.setLastLogTerm(random.nextLong()); 218 | request.setSource(random.nextInt()); 219 | request.setTerm(random.nextLong()); 220 | LogEntry[] entries = new LogEntry[random.nextInt(20) + 1]; 221 | for(int i = 0; i < entries.length; ++i){ 222 | entries[i] = randomLogEntry(); 223 | } 224 | 225 | request.setLogEntries(entries); 226 | return request; 227 | } 228 | 229 | private static RaftMessageType randomMessageType(){ 230 | byte value = (byte)random.nextInt(5); 231 | return RaftMessageType.fromByte((byte) (value + 1)); 232 | } 233 | 234 | private static LogEntry randomLogEntry(){ 235 | byte[] value = new byte[random.nextInt(20) + 1]; 236 | long term = random.nextLong(); 237 | random.nextBytes(value); 238 | return new LogEntry(term, value, LogValueType.fromByte((byte)(random.nextInt(4) + 1))); 239 | } 240 | 241 | static class Pair{ 242 | private T1 item1; 243 | private T2 item2; 244 | 245 | public Pair(T1 item1, T2 item2){ 246 | this.item1 = item1; 247 | this.item2 = item2; 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /dmprinter/src/main/java/net/data/technology/jraft/DummyMessageHandler.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft; 19 | 20 | import java.util.Calendar; 21 | import java.util.Random; 22 | 23 | import org.apache.log4j.LogManager; 24 | 25 | public class DummyMessageHandler implements RaftMessageHandler { 26 | 27 | private Random random = new Random(Calendar.getInstance().getTimeInMillis()); 28 | private org.apache.log4j.Logger logger = LogManager.getLogger(getClass()); 29 | 30 | @Override 31 | public RaftResponseMessage processRequest(RaftRequestMessage request) { 32 | String log = String.format( 33 | "Receive a request(Source: %d, Destination: %d, Term: %d, LLI: %d, LLT: %d, CI: %d, LEL: %d", 34 | request.getSource(), 35 | request.getDestination(), 36 | request.getTerm(), 37 | request.getLastLogIndex(), 38 | request.getLastLogTerm(), 39 | request.getCommitIndex(), 40 | request.getLogEntries() == null ? 0 : request.getLogEntries().length); 41 | logger.debug(log); 42 | System.out.println(log); 43 | return this.randomResponse(request.getSource(), request.getTerm()); 44 | } 45 | 46 | private RaftMessageType randomMessageType(){ 47 | byte value = (byte)this.random.nextInt(5); 48 | return RaftMessageType.fromByte((byte) (value + 1)); 49 | } 50 | 51 | private RaftResponseMessage randomResponse(int source, long term){ 52 | RaftResponseMessage response = new RaftResponseMessage(); 53 | response.setMessageType(this.randomMessageType()); 54 | response.setAccepted(this.random.nextBoolean()); 55 | response.setDestination(source); 56 | response.setSource(this.random.nextInt()); 57 | response.setTerm(term); 58 | response.setNextIndex(this.random.nextLong()); 59 | return response; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /exts/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | 5 | 6 | net.data-technology.jraft 7 | jraft-root 8 | 1.0 9 | 10 | 11 | net.data-technology.jraft 12 | jraft-exts 13 | 1.0.0 14 | jar 15 | 16 | jraft-exts 17 | https://github.com/datatechnology/jraft 18 | extension project for Jraft (Java implementation of Raft Consensus), this module contains may default implementations for interfaces that required by Jraft 19 | 20 | 21 | 22 | Apache License, Version 2.0 23 | http://www.apache.org/licenses/LICENSE-2.0.txt 24 | repo 25 | 26 | 27 | 28 | 29 | 30 | techadmin@data-technology.net 31 | datatech 32 | Data Technology LLC 33 | http://www.data-technology.net 34 | 35 | 36 | 37 | 38 | https://github.com/datatechnology/jraft.git 39 | 40 | 41 | 42 | UTF-8 43 | 44 | 45 | 46 | 47 | junit 48 | junit 49 | test 50 | 51 | 52 | 53 | log4j 54 | log4j 55 | 56 | 57 | 58 | net.data-technology.jraft 59 | jraft-core 60 | 1.0.0 61 | 62 | 63 | 64 | javax.servlet 65 | javax.servlet-api 66 | provided 67 | 68 | 69 | 70 | com.google.code.gson 71 | gson 72 | 73 | 74 | 75 | org.apache.httpcomponents 76 | httpasyncclient 77 | 78 | 79 | 80 | com.h2database 81 | h2 82 | 83 | 84 | 85 | 86 | 87 | ossrh 88 | https://oss.sonatype.org/content/repositories/snapshots 89 | 90 | 91 | 92 | 93 | 94 | 95 | org.apache.maven.plugins 96 | maven-compiler-plugin 97 | 3.5.1 98 | 99 | 1.8 100 | 1.8 101 | 102 | 103 | 104 | org.sonatype.plugins 105 | nexus-staging-maven-plugin 106 | 1.6.7 107 | true 108 | 109 | ossrh 110 | https://oss.sonatype.org/ 111 | true 112 | 113 | 114 | 115 | org.apache.maven.plugins 116 | maven-source-plugin 117 | 2.2.1 118 | 119 | 120 | attach-sources 121 | 122 | jar-no-fork 123 | 124 | 125 | 126 | 127 | 128 | org.apache.maven.plugins 129 | maven-javadoc-plugin 130 | 2.9.1 131 | 132 | 133 | attach-javadocs 134 | 135 | jar 136 | 137 | 138 | 139 | 140 | 141 | org.apache.maven.plugins 142 | maven-gpg-plugin 143 | 1.5 144 | 145 | 146 | sign-artifacts 147 | verify 148 | 149 | sign 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /exts/src/main/java/net/data/technology/jraft/extensions/AsyncUtility.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.extensions; 19 | 20 | import java.nio.ByteBuffer; 21 | import java.nio.channels.AsynchronousByteChannel; 22 | import java.nio.channels.CompletionHandler; 23 | import java.util.function.BiConsumer; 24 | 25 | public class AsyncUtility { 26 | 27 | public static CompletionHandler handlerFrom(BiConsumer completed, BiConsumer failed) { 28 | return new CompletionHandler() { 29 | @Override 30 | public void completed(V result, A attachment) { 31 | completed.accept(result, attachment); 32 | } 33 | 34 | @Override 35 | public void failed(Throwable exc, A attachment) { 36 | failed.accept(exc, attachment); 37 | } 38 | }; 39 | } 40 | 41 | public static void readFromChannel(AsynchronousByteChannel channel, ByteBuffer buffer, A attachment, CompletionHandler completionHandler){ 42 | try{ 43 | channel.read( 44 | buffer, 45 | new AsyncContext(attachment, completionHandler), 46 | handlerFrom( 47 | (Integer result, AsyncContext a) -> { 48 | int bytesRead = result.intValue(); 49 | if(bytesRead == -1 || !buffer.hasRemaining()){ 50 | a.completionHandler.completed(buffer.position(), a.attachment); 51 | }else{ 52 | readFromChannel(channel, buffer, a.attachment, a.completionHandler); 53 | } 54 | }, 55 | (Throwable error, AsyncContext a) -> { 56 | a.completionHandler.failed(error, a.attachment); 57 | })); 58 | }catch(Throwable exception){ 59 | completionHandler.failed(exception, attachment); 60 | } 61 | } 62 | 63 | public static void writeToChannel(AsynchronousByteChannel channel, ByteBuffer buffer, A attachment, CompletionHandler completionHandler){ 64 | try{ 65 | channel.write( 66 | buffer, 67 | new AsyncContext(attachment, completionHandler), 68 | handlerFrom( 69 | (Integer result, AsyncContext a) -> { 70 | int bytesRead = result.intValue(); 71 | if(bytesRead == -1 || !buffer.hasRemaining()){ 72 | a.completionHandler.completed(buffer.position(), a.attachment); 73 | }else{ 74 | writeToChannel(channel, buffer, a.attachment, a.completionHandler); 75 | } 76 | }, 77 | (Throwable error, AsyncContext a) -> { 78 | a.completionHandler.failed(error, a.attachment); 79 | })); 80 | }catch(Throwable exception){ 81 | completionHandler.failed(exception, attachment); 82 | } 83 | } 84 | 85 | static class AsyncContext{ 86 | private A attachment; 87 | private CompletionHandler completionHandler; 88 | 89 | public AsyncContext(A attachment, CompletionHandler handler){ 90 | this.attachment = attachment; 91 | this.completionHandler = handler; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /exts/src/main/java/net/data/technology/jraft/extensions/BinaryUtils.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.extensions; 19 | 20 | import java.io.ByteArrayOutputStream; 21 | import java.io.IOException; 22 | import java.nio.ByteBuffer; 23 | import java.util.ArrayList; 24 | import java.util.List; 25 | 26 | import org.apache.log4j.LogManager; 27 | 28 | import net.data.technology.jraft.LogEntry; 29 | import net.data.technology.jraft.LogValueType; 30 | import net.data.technology.jraft.RaftMessageType; 31 | import net.data.technology.jraft.RaftRequestMessage; 32 | import net.data.technology.jraft.RaftResponseMessage; 33 | 34 | public class BinaryUtils { 35 | 36 | public static final int RAFT_RESPONSE_HEADER_SIZE = Integer.BYTES * 2 + Long.BYTES * 2 + 2; 37 | public static final int RAFT_REQUEST_HEADER_SIZE = Integer.BYTES * 3 + Long.BYTES * 4 + 1; 38 | 39 | public static byte[] longToBytes(long value){ 40 | ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES); 41 | buffer.putLong(value); 42 | return buffer.array(); 43 | } 44 | 45 | public static long bytesToLong(byte[] bytes, int offset){ 46 | ByteBuffer buffer = ByteBuffer.wrap(bytes, offset, Long.BYTES); 47 | return buffer.getLong(); 48 | } 49 | 50 | public static byte[] intToBytes(int value){ 51 | ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES); 52 | buffer.putInt(value); 53 | return buffer.array(); 54 | } 55 | 56 | public static int bytesToInt(byte[] bytes, int offset){ 57 | ByteBuffer buffer = ByteBuffer.wrap(bytes, offset, Integer.BYTES); 58 | return buffer.getInt(); 59 | } 60 | 61 | public static byte booleanToByte(boolean value){ 62 | return value ? (byte)1 : (byte)0; 63 | } 64 | 65 | public static boolean byteToBoolean(byte value){ 66 | return value != 0; 67 | } 68 | 69 | public static byte[] messageToBytes(RaftResponseMessage response){ 70 | ByteBuffer buffer = ByteBuffer.allocate(RAFT_RESPONSE_HEADER_SIZE); 71 | buffer.put(response.getMessageType().toByte()); 72 | buffer.put(intToBytes(response.getSource())); 73 | buffer.put(intToBytes(response.getDestination())); 74 | buffer.put(longToBytes(response.getTerm())); 75 | buffer.put(longToBytes(response.getNextIndex())); 76 | buffer.put(booleanToByte(response.isAccepted())); 77 | return buffer.array(); 78 | } 79 | 80 | public static RaftResponseMessage bytesToResponseMessage(byte[] data){ 81 | if(data == null || data.length != RAFT_RESPONSE_HEADER_SIZE){ 82 | throw new IllegalArgumentException(String.format("data must have %d bytes for a raft response message", RAFT_RESPONSE_HEADER_SIZE)); 83 | } 84 | 85 | ByteBuffer buffer = ByteBuffer.wrap(data); 86 | RaftResponseMessage response = new RaftResponseMessage(); 87 | response.setMessageType(RaftMessageType.fromByte(buffer.get())); 88 | response.setSource(buffer.getInt()); 89 | response.setDestination(buffer.getInt()); 90 | response.setTerm(buffer.getLong()); 91 | response.setNextIndex(buffer.getLong()); 92 | response.setAccepted(buffer.get() == 1); 93 | return response; 94 | } 95 | 96 | public static byte[] messageToBytes(RaftRequestMessage request){ 97 | LogEntry[] logEntries = request.getLogEntries(); 98 | int logSize = 0; 99 | List buffersForLogs = null; 100 | if(logEntries != null && logEntries.length > 0){ 101 | buffersForLogs = new ArrayList(logEntries.length); 102 | for(LogEntry logEntry : logEntries){ 103 | byte[] logData = logEntryToBytes(logEntry); 104 | logSize += logData.length; 105 | buffersForLogs.add(logData); 106 | } 107 | } 108 | 109 | ByteBuffer requestBuffer = ByteBuffer.allocate(RAFT_REQUEST_HEADER_SIZE + logSize); 110 | requestBuffer.put(request.getMessageType().toByte()); 111 | requestBuffer.put(intToBytes(request.getSource())); 112 | requestBuffer.put(intToBytes(request.getDestination())); 113 | requestBuffer.put(longToBytes(request.getTerm())); 114 | requestBuffer.put(longToBytes(request.getLastLogTerm())); 115 | requestBuffer.put(longToBytes(request.getLastLogIndex())); 116 | requestBuffer.put(longToBytes(request.getCommitIndex())); 117 | requestBuffer.put(intToBytes(logSize)); 118 | if(buffersForLogs != null){ 119 | for(byte[] logData : buffersForLogs){ 120 | requestBuffer.put(logData); 121 | } 122 | } 123 | 124 | return requestBuffer.array(); 125 | } 126 | 127 | public static Pair bytesToRequestMessage(byte[] data){ 128 | if(data == null || data.length != RAFT_REQUEST_HEADER_SIZE){ 129 | throw new IllegalArgumentException("invalid request message header."); 130 | } 131 | 132 | ByteBuffer buffer = ByteBuffer.wrap(data); 133 | RaftRequestMessage request = new RaftRequestMessage(); 134 | request.setMessageType(RaftMessageType.fromByte(buffer.get())); 135 | request.setSource(buffer.getInt()); 136 | request.setDestination(buffer.getInt()); 137 | request.setTerm(buffer.getLong()); 138 | request.setLastLogTerm(buffer.getLong()); 139 | request.setLastLogIndex(buffer.getLong()); 140 | request.setCommitIndex(buffer.getLong()); 141 | int logDataSize = buffer.getInt(); 142 | return new Pair(request, logDataSize); 143 | } 144 | 145 | public static byte[] logEntryToBytes(LogEntry logEntry){ 146 | ByteArrayOutputStream output = new ByteArrayOutputStream(); 147 | try{ 148 | output.write(longToBytes(logEntry.getTerm())); 149 | output.write(logEntry.getValueType().toByte()); 150 | output.write(intToBytes(logEntry.getValue().length)); 151 | output.write(logEntry.getValue()); 152 | output.flush(); 153 | return output.toByteArray(); 154 | }catch(IOException exception){ 155 | LogManager.getLogger("BinaryUtil").error("failed to serialize LogEntry to memory", exception); 156 | throw new RuntimeException("Running into bad situation, where memory may not be sufficient", exception); 157 | } 158 | } 159 | 160 | public static LogEntry[] bytesToLogEntries(byte[] data){ 161 | if(data == null || data.length < Long.BYTES + Integer.BYTES){ 162 | throw new IllegalArgumentException("invalid log entries data"); 163 | } 164 | ByteBuffer buffer = ByteBuffer.wrap(data); 165 | List logEntries = new ArrayList(); 166 | while(buffer.hasRemaining()){ 167 | long term = buffer.getLong(); 168 | byte valueType = buffer.get(); 169 | int valueSize = buffer.getInt(); 170 | byte[] value = new byte[valueSize]; 171 | if(valueSize > 0){ 172 | buffer.get(value); 173 | } 174 | logEntries.add(new LogEntry(term, value, LogValueType.fromByte(valueType))); 175 | } 176 | 177 | return logEntries.toArray(new LogEntry[0]); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /exts/src/main/java/net/data/technology/jraft/extensions/FileBasedServerStateManager.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.extensions; 19 | 20 | import java.io.FileInputStream; 21 | import java.io.FileOutputStream; 22 | import java.io.IOException; 23 | import java.io.InputStreamReader; 24 | import java.io.RandomAccessFile; 25 | import java.nio.ByteBuffer; 26 | import java.nio.charset.StandardCharsets; 27 | import java.nio.file.Files; 28 | import java.nio.file.Path; 29 | import java.nio.file.Paths; 30 | import java.util.Properties; 31 | 32 | import org.apache.log4j.LogManager; 33 | import org.apache.log4j.Logger; 34 | 35 | import com.google.gson.Gson; 36 | import com.google.gson.GsonBuilder; 37 | 38 | import net.data.technology.jraft.ClusterConfiguration; 39 | import net.data.technology.jraft.SequentialLogStore; 40 | import net.data.technology.jraft.ServerState; 41 | import net.data.technology.jraft.ServerStateManager; 42 | 43 | public class FileBasedServerStateManager implements ServerStateManager { 44 | 45 | private static final String STATE_FILE = "server.state"; 46 | private static final String CONFIG_FILE = "config.properties"; 47 | private static final String CLUSTER_CONFIG_FILE = "cluster.json"; 48 | 49 | private RandomAccessFile serverStateFile; 50 | private FileBasedSequentialLogStore logStore; 51 | private Logger logger; 52 | private Path container; 53 | private int serverId; 54 | 55 | public FileBasedServerStateManager(String dataDirectory){ 56 | this.logStore = new FileBasedSequentialLogStore(dataDirectory); 57 | this.container = Paths.get(dataDirectory); 58 | this.logger = LogManager.getLogger(getClass()); 59 | try{ 60 | Properties props = new Properties(); 61 | FileInputStream configInput = new FileInputStream(this.container.resolve(CONFIG_FILE).toString()); 62 | props.load(configInput); 63 | String serverIdValue = props.getProperty("server.id"); 64 | this.serverId = serverIdValue == null || serverIdValue.length() == 0 ? -1 : Integer.parseInt(serverIdValue.trim()); 65 | configInput.close(); 66 | this.serverStateFile = new RandomAccessFile(this.container.resolve(STATE_FILE).toString(), "rw"); 67 | this.serverStateFile.seek(0); 68 | }catch(IOException exception){ 69 | this.logger.error("failed to create/open server state file", exception); 70 | throw new IllegalArgumentException("cannot create/open the state file", exception); 71 | } 72 | } 73 | 74 | @Override 75 | public ClusterConfiguration loadClusterConfiguration(){ 76 | Gson gson = new GsonBuilder().create(); 77 | FileInputStream stream = null; 78 | 79 | try{ 80 | stream = new FileInputStream(this.container.resolve(CLUSTER_CONFIG_FILE).toString()); 81 | ClusterConfiguration config = gson.fromJson(new InputStreamReader(stream, StandardCharsets.UTF_8), ClusterConfiguration.class); 82 | return config; 83 | }catch(IOException error){ 84 | this.logger.error("failed to read cluster configuration", error); 85 | throw new RuntimeException("failed to read in cluster config", error); 86 | }finally{ 87 | if(stream != null){ 88 | try{ 89 | stream.close(); 90 | }catch(Exception e){ 91 | //ignore the error 92 | } 93 | } 94 | } 95 | } 96 | 97 | public void saveClusterConfiguration(ClusterConfiguration configuration){ 98 | Gson gson = new GsonBuilder().create(); 99 | String configData = gson.toJson(configuration); 100 | try { 101 | Files.deleteIfExists(this.container.resolve(CLUSTER_CONFIG_FILE)); 102 | FileOutputStream output = new FileOutputStream(this.container.resolve(CLUSTER_CONFIG_FILE).toString()); 103 | output.write(configData.getBytes(StandardCharsets.UTF_8)); 104 | output.flush(); 105 | output.close(); 106 | } catch (IOException error) { 107 | this.logger.error("failed to save cluster config to file", error); 108 | } 109 | } 110 | 111 | public int getServerId(){ 112 | return this.serverId; 113 | } 114 | 115 | @Override 116 | public synchronized void persistState(ServerState serverState) { 117 | try{ 118 | ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES * 2 + Integer.BYTES); 119 | buffer.putLong(serverState.getTerm()); 120 | buffer.putLong(serverState.getCommitIndex()); 121 | buffer.putInt(serverState.getVotedFor()); 122 | this.serverStateFile.write(buffer.array()); 123 | this.serverStateFile.seek(0); 124 | }catch(IOException ioError){ 125 | this.logger.error("failed to write to the server state file", ioError); 126 | throw new RuntimeException("fatal I/O error while writing to the state file", ioError); 127 | } 128 | } 129 | 130 | @Override 131 | public synchronized ServerState readState() { 132 | try{ 133 | if(this.serverStateFile.length() == 0){ 134 | return null; 135 | } 136 | 137 | byte[] stateData = new byte[Long.BYTES * 2 + Integer.BYTES]; 138 | this.read(stateData); 139 | this.serverStateFile.seek(0); 140 | ByteBuffer buffer = ByteBuffer.wrap(stateData); 141 | ServerState state = new ServerState(); 142 | state.setTerm(buffer.getLong()); 143 | state.setCommitIndex(buffer.getLong()); 144 | state.setVotedFor(buffer.getInt()); 145 | return state; 146 | }catch(IOException ioError){ 147 | this.logger.error("failed to read from the server state file", ioError); 148 | throw new RuntimeException("fatal I/O error while reading from state file", ioError); 149 | } 150 | } 151 | 152 | @Override 153 | public SequentialLogStore loadLogStore() { 154 | return this.logStore; 155 | } 156 | 157 | public void close(){ 158 | try{ 159 | this.serverStateFile.close(); 160 | this.logStore.close(); 161 | }catch(IOException exception){ 162 | this.logger.info("failed to shutdown the server state manager due to io error", exception); 163 | } 164 | } 165 | 166 | private void read(byte[] buffer){ 167 | try{ 168 | int offset = 0; 169 | int bytesRead = 0; 170 | while(offset < buffer.length && (bytesRead = this.serverStateFile.read(buffer, offset, buffer.length - offset)) != -1){ 171 | offset += bytesRead; 172 | } 173 | 174 | if(offset < buffer.length){ 175 | this.logger.error(String.format("only %d bytes are read while %d bytes are desired, bad file", offset, buffer.length)); 176 | throw new RuntimeException("bad file, insufficient file data for reading"); 177 | } 178 | }catch(IOException exception){ 179 | this.logger.error("failed to read and fill the buffer", exception); 180 | throw new RuntimeException(exception.getMessage(), exception); 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /exts/src/main/java/net/data/technology/jraft/extensions/Log4jLogger.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.extensions; 19 | 20 | import org.apache.log4j.Logger; 21 | 22 | public class Log4jLogger implements net.data.technology.jraft.Logger { 23 | 24 | private Logger logger; 25 | 26 | public Log4jLogger(Logger logger){ 27 | this.logger = logger; 28 | } 29 | 30 | public void debug(String format, Object... args) { 31 | if(args != null){ 32 | this.logger.debug(String.format(format, args)); 33 | }else{ 34 | this.logger.debug(format); 35 | } 36 | } 37 | 38 | public void info(String format, Object... args) { 39 | if(args != null){ 40 | this.logger.info(String.format(format, args)); 41 | }else{ 42 | this.logger.info(format); 43 | } 44 | } 45 | 46 | public void warning(String format, Object... args) { 47 | if(args != null){ 48 | this.logger.warn(String.format(format, args)); 49 | }else{ 50 | this.logger.warn(format); 51 | } 52 | } 53 | 54 | public void error(String format, Object... args) { 55 | if(args != null){ 56 | this.logger.error(String.format(format, args)); 57 | }else{ 58 | this.logger.error(format); 59 | } 60 | } 61 | 62 | @Override 63 | public void error(String format, Throwable error, Object... args) { 64 | if(args != null){ 65 | this.logger.error(String.format(format, args), error); 66 | }else{ 67 | this.logger.error(format, error); 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /exts/src/main/java/net/data/technology/jraft/extensions/Log4jLoggerFactory.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.extensions; 19 | 20 | import org.apache.log4j.LogManager; 21 | 22 | import net.data.technology.jraft.Logger; 23 | import net.data.technology.jraft.LoggerFactory; 24 | 25 | public class Log4jLoggerFactory implements LoggerFactory { 26 | 27 | public Logger getLogger(Class clazz) { 28 | return new Log4jLogger(LogManager.getLogger(clazz)); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /exts/src/main/java/net/data/technology/jraft/extensions/Pair.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.extensions; 19 | 20 | public class Pair { 21 | 22 | private TFirst first; 23 | private TSecond second; 24 | 25 | public Pair(TFirst first, TSecond second){ 26 | this.first = first; 27 | this.second = second; 28 | } 29 | 30 | public TFirst getFirst(){ 31 | return this.first; 32 | } 33 | 34 | public TSecond getSecond(){ 35 | return this.second; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /exts/src/main/java/net/data/technology/jraft/extensions/RpcTcpClient.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.extensions; 19 | 20 | import java.io.IOException; 21 | import java.net.InetSocketAddress; 22 | import java.nio.ByteBuffer; 23 | import java.nio.channels.AsynchronousChannelGroup; 24 | import java.nio.channels.AsynchronousSocketChannel; 25 | import java.nio.channels.CompletionHandler; 26 | import java.util.concurrent.CompletableFuture; 27 | import java.util.concurrent.ConcurrentLinkedQueue; 28 | import java.util.concurrent.ExecutorService; 29 | import java.util.concurrent.atomic.AtomicInteger; 30 | import java.util.function.BiConsumer; 31 | 32 | import org.apache.log4j.LogManager; 33 | import org.apache.log4j.Logger; 34 | 35 | import net.data.technology.jraft.RaftRequestMessage; 36 | import net.data.technology.jraft.RaftResponseMessage; 37 | import net.data.technology.jraft.RpcClient; 38 | 39 | public class RpcTcpClient implements RpcClient { 40 | 41 | private AsynchronousSocketChannel connection; 42 | private AsynchronousChannelGroup channelGroup; 43 | private ConcurrentLinkedQueue> readTasks; 44 | private ConcurrentLinkedQueue> writeTasks; 45 | private AtomicInteger readers; 46 | private AtomicInteger writers; 47 | private InetSocketAddress remote; 48 | private Logger logger; 49 | 50 | public RpcTcpClient(InetSocketAddress remote, ExecutorService executorService){ 51 | this.remote = remote; 52 | this.logger = LogManager.getLogger(getClass()); 53 | this.readTasks = new ConcurrentLinkedQueue>(); 54 | this.writeTasks = new ConcurrentLinkedQueue>(); 55 | this.readers = new AtomicInteger(0); 56 | this.writers = new AtomicInteger(0); 57 | try{ 58 | this.channelGroup = AsynchronousChannelGroup.withThreadPool(executorService); 59 | }catch(IOException err){ 60 | this.logger.error("failed to create channel group", err); 61 | throw new RuntimeException("failed to create the channel group due to errors."); 62 | } 63 | } 64 | 65 | @Override 66 | public synchronized CompletableFuture send(final RaftRequestMessage request) { 67 | this.logger.debug(String.format("trying to send message %s to server %d at endpoint %s", request.getMessageType().toString(), request.getDestination(), this.remote.toString())); 68 | CompletableFuture result = new CompletableFuture(); 69 | if(this.connection == null || !this.connection.isOpen()){ 70 | try{ 71 | this.connection = AsynchronousSocketChannel.open(this.channelGroup); 72 | this.connection.connect(this.remote, new AsyncTask(request, result), handlerFrom((Void v, AsyncTask task) -> { 73 | sendAndRead(task, false); 74 | })); 75 | }catch(Throwable error){ 76 | closeSocket(); 77 | result.completeExceptionally(error); 78 | } 79 | }else{ 80 | this.sendAndRead(new AsyncTask(request, result), false); 81 | } 82 | 83 | return result; 84 | } 85 | 86 | private void sendAndRead(AsyncTask task, boolean skipQueueing){ 87 | if(!skipQueueing){ 88 | int writerCount = this.writers.getAndIncrement(); 89 | if(writerCount > 0){ 90 | this.logger.debug("there is a pending write, queue this write task"); 91 | this.writeTasks.add(task); 92 | return; 93 | } 94 | } 95 | 96 | ByteBuffer buffer = ByteBuffer.wrap(BinaryUtils.messageToBytes(task.input)); 97 | try{ 98 | AsyncUtility.writeToChannel(this.connection, buffer, task, handlerFrom((Integer bytesSent, AsyncTask context) -> { 99 | if(bytesSent.intValue() < buffer.limit()){ 100 | logger.info("failed to sent the request to remote server."); 101 | context.future.completeExceptionally(new IOException("Only partial of the data could be sent")); 102 | closeSocket(); 103 | }else{ 104 | // read the response 105 | ByteBuffer responseBuffer = ByteBuffer.allocate(BinaryUtils.RAFT_RESPONSE_HEADER_SIZE); 106 | this.readResponse(new AsyncTask(responseBuffer, context.future), false); 107 | } 108 | 109 | int waitingWriters = this.writers.decrementAndGet(); 110 | if(waitingWriters > 0){ 111 | this.logger.debug("there are pending writers in queue, will try to process them"); 112 | AsyncTask pendingTask = null; 113 | while((pendingTask = this.writeTasks.poll()) == null); 114 | this.sendAndRead(pendingTask, true); 115 | } 116 | })); 117 | }catch(Exception writeError){ 118 | logger.info("failed to write the socket", writeError); 119 | task.future.completeExceptionally(writeError); 120 | closeSocket(); 121 | } 122 | } 123 | 124 | private void readResponse(AsyncTask task, boolean skipQueueing){ 125 | if(!skipQueueing){ 126 | int readerCount = this.readers.getAndIncrement(); 127 | if(readerCount > 0){ 128 | this.logger.debug("there is a pending read, queue this read task"); 129 | this.readTasks.add(task); 130 | return; 131 | } 132 | } 133 | 134 | CompletionHandler> handler = handlerFrom((Integer bytesRead, AsyncTask context) -> { 135 | if(bytesRead.intValue() < BinaryUtils.RAFT_RESPONSE_HEADER_SIZE){ 136 | logger.info("failed to read response from remote server."); 137 | context.future.completeExceptionally(new IOException("Only part of the response data could be read")); 138 | closeSocket(); 139 | }else{ 140 | RaftResponseMessage response = BinaryUtils.bytesToResponseMessage(context.input.array()); 141 | context.future.complete(response); 142 | } 143 | 144 | int waitingReaders = this.readers.decrementAndGet(); 145 | if(waitingReaders > 0){ 146 | this.logger.debug("there are pending readers in queue, will try to process them"); 147 | AsyncTask pendingTask = null; 148 | while((pendingTask = this.readTasks.poll()) == null); 149 | this.readResponse(pendingTask, true); 150 | } 151 | }); 152 | 153 | try{ 154 | this.logger.debug("reading response from socket..."); 155 | AsyncUtility.readFromChannel(this.connection, task.input, task, handler); 156 | }catch(Exception readError){ 157 | logger.info("failed to read from socket", readError); 158 | task.future.completeExceptionally(readError); 159 | closeSocket(); 160 | } 161 | } 162 | 163 | private CompletionHandler> handlerFrom(BiConsumer> completed) { 164 | return AsyncUtility.handlerFrom(completed, (Throwable error, AsyncTask context) -> { 165 | this.logger.info("socket error", error); 166 | context.future.completeExceptionally(error); 167 | closeSocket(); 168 | }); 169 | } 170 | 171 | private synchronized void closeSocket(){ 172 | this.logger.debug("close the socket due to errors"); 173 | try{ 174 | if(this.connection != null){ 175 | this.connection.close(); 176 | this.connection = null; 177 | } 178 | }catch(IOException ex){ 179 | this.logger.info("failed to close socket", ex); 180 | } 181 | 182 | while(true){ 183 | AsyncTask task = this.readTasks.poll(); 184 | if(task == null){ 185 | break; 186 | } 187 | 188 | task.future.completeExceptionally(new IOException("socket is closed, no more reads can be completed")); 189 | } 190 | 191 | while(true){ 192 | AsyncTask task = this.writeTasks.poll(); 193 | if(task == null){ 194 | break; 195 | } 196 | 197 | task.future.completeExceptionally(new IOException("socket is closed, no more writes can be completed")); 198 | } 199 | 200 | this.readers.set(0); 201 | this.writers.set(0); 202 | } 203 | 204 | static class AsyncTask{ 205 | private TInput input; 206 | private CompletableFuture future; 207 | 208 | public AsyncTask(TInput input, CompletableFuture future){ 209 | this.input = input; 210 | this.future = future; 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /exts/src/main/java/net/data/technology/jraft/extensions/RpcTcpClientFactory.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.extensions; 19 | 20 | import java.net.InetSocketAddress; 21 | import java.net.URI; 22 | import java.net.URISyntaxException; 23 | import java.util.concurrent.ExecutorService; 24 | 25 | import org.apache.log4j.LogManager; 26 | 27 | import net.data.technology.jraft.RpcClient; 28 | import net.data.technology.jraft.RpcClientFactory; 29 | 30 | public class RpcTcpClientFactory implements RpcClientFactory { 31 | private ExecutorService executorService; 32 | 33 | public RpcTcpClientFactory(ExecutorService executorService){ 34 | this.executorService = executorService; 35 | } 36 | 37 | @Override 38 | public RpcClient createRpcClient(String endpoint) { 39 | try { 40 | URI uri = new URI(endpoint); 41 | return new RpcTcpClient(new InetSocketAddress(uri.getHost(), uri.getPort()), this.executorService); 42 | } catch (URISyntaxException e) { 43 | LogManager.getLogger(getClass()).error(String.format("%s is not a valid uri", endpoint)); 44 | throw new IllegalArgumentException("invalid uri for endpoint"); 45 | } 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /exts/src/main/java/net/data/technology/jraft/extensions/RpcTcpListener.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.extensions; 19 | 20 | import java.io.IOException; 21 | import java.net.InetSocketAddress; 22 | import java.net.StandardSocketOptions; 23 | import java.nio.ByteBuffer; 24 | import java.nio.channels.AsynchronousChannelGroup; 25 | import java.nio.channels.AsynchronousServerSocketChannel; 26 | import java.nio.channels.AsynchronousSocketChannel; 27 | import java.nio.channels.CompletionHandler; 28 | import java.util.Collections; 29 | import java.util.LinkedList; 30 | import java.util.List; 31 | import java.util.concurrent.ExecutorService; 32 | import java.util.function.BiConsumer; 33 | 34 | import org.apache.log4j.LogManager; 35 | import org.apache.log4j.Logger; 36 | 37 | import net.data.technology.jraft.RaftMessageHandler; 38 | import net.data.technology.jraft.RaftRequestMessage; 39 | import net.data.technology.jraft.RaftResponseMessage; 40 | import net.data.technology.jraft.RpcListener; 41 | 42 | public class RpcTcpListener implements RpcListener { 43 | private int port; 44 | private Logger logger; 45 | private AsynchronousServerSocketChannel listener; 46 | private ExecutorService executorService; 47 | private List connections; 48 | 49 | public RpcTcpListener(int port, ExecutorService executorService){ 50 | this.port = port; 51 | this.executorService = executorService; 52 | this.logger = LogManager.getLogger(getClass()); 53 | this.connections = Collections.synchronizedList(new LinkedList()); 54 | } 55 | 56 | @Override 57 | public void startListening(RaftMessageHandler messageHandler) { 58 | try{ 59 | AsynchronousChannelGroup channelGroup = AsynchronousChannelGroup.withThreadPool(this.executorService); 60 | this.listener = AsynchronousServerSocketChannel.open(channelGroup); 61 | this.listener.setOption(StandardSocketOptions.SO_REUSEADDR, true); 62 | this.listener.bind(new InetSocketAddress(this.port)); 63 | this.acceptRequests(messageHandler); 64 | }catch(IOException exception){ 65 | logger.error("failed to start the listener due to io error", exception); 66 | } 67 | } 68 | 69 | @Override 70 | public void stop(){ 71 | for(AsynchronousSocketChannel connection : this.connections){ 72 | try{ 73 | connection.close(); 74 | }catch(IOException error){ 75 | logger.info("failed to close connection, but it's fine", error); 76 | } 77 | } 78 | 79 | if(this.listener != null){ 80 | try { 81 | this.listener.close(); 82 | } catch (IOException e) { 83 | logger.info("failed to close the listener socket", e); 84 | } 85 | 86 | this.listener = null; 87 | } 88 | 89 | if(this.executorService != null){ 90 | this.executorService.shutdown(); 91 | this.executorService = null; 92 | } 93 | } 94 | 95 | private void acceptRequests(RaftMessageHandler messageHandler){ 96 | try{ 97 | this.listener.accept(messageHandler, AsyncUtility.handlerFrom( 98 | (AsynchronousSocketChannel connection, RaftMessageHandler handler) -> { 99 | connections.add(connection); 100 | acceptRequests(handler); 101 | readRequest(connection, handler); 102 | }, 103 | (Throwable error, RaftMessageHandler handler) -> { 104 | logger.error("accepting a new connection failed, will still keep accepting more requests", error); 105 | acceptRequests(handler); 106 | })); 107 | }catch(Exception exception){ 108 | logger.error("failed to accept new requests, will retry", exception); 109 | this.acceptRequests(messageHandler); 110 | } 111 | } 112 | 113 | private void readRequest(final AsynchronousSocketChannel connection, RaftMessageHandler messageHandler){ 114 | ByteBuffer buffer = ByteBuffer.allocate(BinaryUtils.RAFT_REQUEST_HEADER_SIZE); 115 | try{ 116 | AsyncUtility.readFromChannel(connection, buffer, messageHandler, handlerFrom((Integer bytesRead, final RaftMessageHandler handler) -> { 117 | if(bytesRead.intValue() < BinaryUtils.RAFT_REQUEST_HEADER_SIZE){ 118 | logger.info("failed to read the request header from client socket"); 119 | closeSocket(connection); 120 | }else{ 121 | try{ 122 | logger.debug("request header read, try to see if there is a request body"); 123 | final Pair requestInfo = BinaryUtils.bytesToRequestMessage(buffer.array()); 124 | if(requestInfo.getSecond().intValue() > 0){ 125 | ByteBuffer logBuffer = ByteBuffer.allocate(requestInfo.getSecond().intValue()); 126 | AsyncUtility.readFromChannel(connection, logBuffer, null, handlerFrom((Integer size, Object attachment) -> { 127 | if(size.intValue() < requestInfo.getSecond().intValue()){ 128 | logger.info("failed to read the log entries data from client socket"); 129 | closeSocket(connection); 130 | }else{ 131 | try{ 132 | requestInfo.getFirst().setLogEntries(BinaryUtils.bytesToLogEntries(logBuffer.array())); 133 | processRequest(connection, requestInfo.getFirst(), handler); 134 | }catch(Throwable error){ 135 | logger.info("log entries parsing error", error); 136 | closeSocket(connection); 137 | } 138 | } 139 | }, connection)); 140 | }else{ 141 | processRequest(connection, requestInfo.getFirst(), handler); 142 | } 143 | }catch(Throwable runtimeError){ 144 | // if there are any conversion errors, we need to close the client socket to prevent more errors 145 | closeSocket(connection); 146 | logger.info("message reading/parsing error", runtimeError); 147 | } 148 | } 149 | }, connection)); 150 | }catch(Exception readError){ 151 | logger.info("failed to read more request from client socket", readError); 152 | closeSocket(connection); 153 | } 154 | } 155 | 156 | private void processRequest(AsynchronousSocketChannel connection, RaftRequestMessage request, RaftMessageHandler messageHandler){ 157 | try{ 158 | RaftResponseMessage response = messageHandler.processRequest(request); 159 | final ByteBuffer buffer = ByteBuffer.wrap(BinaryUtils.messageToBytes(response)); 160 | AsyncUtility.writeToChannel(connection, buffer, null, handlerFrom((Integer bytesSent, Object attachment) -> { 161 | if(bytesSent.intValue() < buffer.limit()){ 162 | logger.info("failed to completely send the response."); 163 | closeSocket(connection); 164 | }else{ 165 | logger.debug("response message sent."); 166 | if(connection.isOpen()){ 167 | logger.debug("try to read next request"); 168 | readRequest(connection, messageHandler); 169 | } 170 | } 171 | }, connection)); 172 | }catch(Throwable error){ 173 | // for any errors, we will close the socket to prevent more errors 174 | closeSocket(connection); 175 | logger.error("failed to process the request or send the response", error); 176 | } 177 | } 178 | 179 | private CompletionHandler handlerFrom(BiConsumer completed, AsynchronousSocketChannel connection) { 180 | return AsyncUtility.handlerFrom(completed, (Throwable error, A attachment) -> { 181 | this.logger.info("socket server failure", error); 182 | if(connection != null){ 183 | closeSocket(connection); 184 | } 185 | }); 186 | } 187 | 188 | private void closeSocket(AsynchronousSocketChannel connection){ 189 | try{ 190 | this.connections.remove(connection); 191 | connection.close(); 192 | }catch(IOException ex){ 193 | this.logger.info("failed to close client socket", ex); 194 | } 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /exts/src/main/java/net/data/technology/jraft/extensions/http/HttpRpcClient.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.extensions.http; 19 | 20 | import java.io.IOException; 21 | import java.io.InputStreamReader; 22 | import java.nio.charset.StandardCharsets; 23 | import java.util.concurrent.CompletableFuture; 24 | 25 | import org.apache.http.HttpResponse; 26 | import org.apache.http.client.methods.HttpPost; 27 | import org.apache.http.concurrent.FutureCallback; 28 | import org.apache.http.entity.StringEntity; 29 | import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; 30 | import org.apache.http.impl.nio.client.HttpAsyncClients; 31 | import org.apache.log4j.LogManager; 32 | import org.apache.log4j.Logger; 33 | 34 | import com.google.gson.Gson; 35 | import com.google.gson.GsonBuilder; 36 | 37 | import net.data.technology.jraft.RaftRequestMessage; 38 | import net.data.technology.jraft.RaftResponseMessage; 39 | import net.data.technology.jraft.RpcClient; 40 | 41 | public class HttpRpcClient implements RpcClient { 42 | private String serverUrl; 43 | private CloseableHttpAsyncClient httpClient; 44 | private Gson gson; 45 | private Logger logger; 46 | 47 | public HttpRpcClient(String serverUrl){ 48 | this.serverUrl = serverUrl; 49 | this.httpClient = HttpAsyncClients.createDefault(); 50 | this.gson = new GsonBuilder().create(); 51 | this.logger = LogManager.getLogger(getClass()); 52 | } 53 | 54 | @Override 55 | public CompletableFuture send(RaftRequestMessage request) { 56 | CompletableFuture future = new CompletableFuture(); 57 | String payload = this.gson.toJson(request); 58 | HttpPost postRequest = new HttpPost(this.serverUrl); 59 | postRequest.setEntity(new StringEntity(payload, StandardCharsets.UTF_8)); 60 | this.httpClient.execute(postRequest, new FutureCallback(){ 61 | 62 | @Override 63 | public void completed(HttpResponse result) { 64 | if(result.getStatusLine().getStatusCode() != 200){ 65 | logger.info("receive an response error code " + String.valueOf(result.getStatusLine().getStatusCode()) + " from server"); 66 | future.completeExceptionally(new IOException("Service Error")); 67 | } 68 | 69 | try{ 70 | InputStreamReader reader = new InputStreamReader(result.getEntity().getContent()); 71 | RaftResponseMessage response = gson.fromJson(reader, RaftResponseMessage.class); 72 | future.complete(response); 73 | }catch(Throwable error){ 74 | logger.info("fails to parse the response from server due to errors", error); 75 | future.completeExceptionally(error); 76 | } 77 | } 78 | 79 | @Override 80 | public void failed(Exception ex) { 81 | future.completeExceptionally(ex); 82 | } 83 | 84 | @Override 85 | public void cancelled() { 86 | future.completeExceptionally(new IOException("request cancelled")); 87 | }}); 88 | return future; 89 | } 90 | 91 | } 92 | -------------------------------------------------------------------------------- /exts/src/main/java/net/data/technology/jraft/extensions/http/HttpRpcClientFactory.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.extensions.http; 19 | 20 | import net.data.technology.jraft.RpcClient; 21 | import net.data.technology.jraft.RpcClientFactory; 22 | 23 | public class HttpRpcClientFactory implements RpcClientFactory { 24 | 25 | @Override 26 | public RpcClient createRpcClient(String endpoint) { 27 | return new HttpRpcClient(endpoint); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /exts/src/main/java/net/data/technology/jraft/extensions/http/JraftServlet.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.extensions.http; 19 | 20 | import java.io.IOException; 21 | import java.io.InputStreamReader; 22 | 23 | import javax.servlet.ServletException; 24 | import javax.servlet.http.HttpServlet; 25 | import javax.servlet.http.HttpServletRequest; 26 | import javax.servlet.http.HttpServletResponse; 27 | 28 | import org.apache.log4j.LogManager; 29 | import org.apache.log4j.Logger; 30 | 31 | import com.google.gson.Gson; 32 | import com.google.gson.GsonBuilder; 33 | 34 | import net.data.technology.jraft.RaftMessageHandler; 35 | import net.data.technology.jraft.RaftRequestMessage; 36 | import net.data.technology.jraft.RaftResponseMessage; 37 | 38 | public class JraftServlet extends HttpServlet { 39 | 40 | private static final long serialVersionUID = -414289039785671159L; 41 | private Logger logger; 42 | private Gson gson; 43 | 44 | public JraftServlet(){ 45 | this.logger = LogManager.getLogger(getClass()); 46 | this.gson = new GsonBuilder().create(); 47 | } 48 | 49 | @Override 50 | protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ 51 | response.getWriter().write("listening."); 52 | } 53 | 54 | @Override 55 | protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{ 56 | RaftMessageHandler messageHandler = JraftServletListener.getMessageHandler(this.getServletContext()); 57 | if(messageHandler == null){ 58 | this.logger.error("JraftServletListener is not setup correctly, no message handler could be found.");; 59 | response.setStatus(503); // Service is not available 60 | response.getWriter().print("Jraft is not yet ready to serve"); 61 | }else{ 62 | InputStreamReader reader = new InputStreamReader(request.getInputStream()); 63 | RaftRequestMessage raftRequest = this.gson.fromJson(reader, RaftRequestMessage.class); 64 | RaftResponseMessage raftResponse = messageHandler.processRequest(raftRequest); 65 | response.setStatus(200); 66 | response.setContentType("application/json"); 67 | response.getWriter().write(this.gson.toJson(raftResponse)); 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /exts/src/main/java/net/data/technology/jraft/extensions/http/JraftServletListener.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.extensions.http; 19 | 20 | import javax.servlet.ServletContext; 21 | import javax.servlet.ServletContextEvent; 22 | import javax.servlet.ServletContextListener; 23 | 24 | import net.data.technology.jraft.LoggerFactory; 25 | import net.data.technology.jraft.RaftConsensus; 26 | import net.data.technology.jraft.RaftContext; 27 | import net.data.technology.jraft.RaftMessageHandler; 28 | import net.data.technology.jraft.RaftMessageSender; 29 | import net.data.technology.jraft.RaftParameters; 30 | import net.data.technology.jraft.RpcListener; 31 | import net.data.technology.jraft.ServerStateManager; 32 | import net.data.technology.jraft.StateMachine; 33 | import net.data.technology.jraft.extensions.Log4jLoggerFactory; 34 | 35 | public abstract class JraftServletListener implements RpcListener, ServletContextListener { 36 | 37 | public static final String JRAFT_MESSAGE_SENDER = "$Jraft$Message$Sender"; 38 | public static final String JRAFT_MESSAGE_HANDLER = "$Jraft$Message$Handler"; 39 | 40 | private ServletContext servletContext; 41 | 42 | public static RaftMessageHandler getMessageHandler(ServletContext context){ 43 | return (RaftMessageHandler)context.getAttribute(JRAFT_MESSAGE_HANDLER); 44 | } 45 | 46 | public static RaftMessageSender getMessageSender(ServletContext context){ 47 | return (RaftMessageSender)context.getAttribute(JRAFT_MESSAGE_SENDER); 48 | } 49 | 50 | @Override 51 | public void contextInitialized(ServletContextEvent sce) { 52 | this.servletContext = sce.getServletContext(); 53 | RaftContext context = new RaftContext( 54 | this.getServerStateManager(), 55 | this.getStateMachine(), 56 | this.getParameters(), 57 | this, 58 | this.getLoggerFactory(), 59 | new HttpRpcClientFactory()); 60 | this.servletContext.setAttribute(JRAFT_MESSAGE_SENDER, RaftConsensus.run(context)); 61 | } 62 | 63 | @Override 64 | public void contextDestroyed(ServletContextEvent sce) { 65 | } 66 | 67 | @Override 68 | public void startListening(RaftMessageHandler messageHandler) { 69 | this.servletContext.setAttribute(JRAFT_MESSAGE_HANDLER, messageHandler); 70 | } 71 | 72 | @Override 73 | public abstract void stop(); 74 | 75 | protected abstract RaftParameters getParameters(); 76 | 77 | protected abstract ServerStateManager getServerStateManager(); 78 | 79 | protected abstract StateMachine getStateMachine(); 80 | 81 | protected LoggerFactory getLoggerFactory(){ 82 | return new Log4jLoggerFactory(); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /exts/src/test/java/net/data/technology/jraft/extensions/BinaryUtilTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.extensions; 19 | 20 | import static org.junit.Assert.*; 21 | 22 | import java.util.Calendar; 23 | import java.util.Random; 24 | 25 | import org.junit.Test; 26 | 27 | import net.data.technology.jraft.LogEntry; 28 | import net.data.technology.jraft.LogValueType; 29 | import net.data.technology.jraft.RaftMessageType; 30 | import net.data.technology.jraft.RaftRequestMessage; 31 | import net.data.technology.jraft.RaftResponseMessage; 32 | 33 | public class BinaryUtilTests { 34 | private Random random = new Random(Calendar.getInstance().getTimeInMillis()); 35 | 36 | @Test 37 | public void testIntegerConverter() { 38 | int value = random.nextInt(); 39 | byte[] buffer = BinaryUtils.intToBytes(value); 40 | int restoredValue = BinaryUtils.bytesToInt(buffer, 0); 41 | assertEquals(value, restoredValue); 42 | } 43 | 44 | @Test 45 | public void testLongConverter(){ 46 | long value = random.nextLong(); 47 | byte[] buffer = BinaryUtils.longToBytes(value); 48 | long restoredValue = BinaryUtils.bytesToLong(buffer, 0); 49 | assertEquals(value, restoredValue); 50 | } 51 | 52 | @Test 53 | public void testBooleanConverter(){ 54 | assertEquals((byte)1, BinaryUtils.booleanToByte(true)); 55 | assertEquals((byte)0, BinaryUtils.booleanToByte(false)); 56 | assertEquals(true, BinaryUtils.byteToBoolean((byte)1)); 57 | assertEquals(false, BinaryUtils.byteToBoolean((byte)0)); 58 | } 59 | 60 | @Test 61 | public void testLogEntryConverter(){ 62 | LogEntry logEntry = this.randomLogEntry(); 63 | byte[] data = BinaryUtils.logEntryToBytes(logEntry); 64 | LogEntry[] entries = BinaryUtils.bytesToLogEntries(data); 65 | assertTrue(entries != null); 66 | assertEquals(1, entries.length); 67 | assertTrue(logEntriesEquals(logEntry, entries[0])); 68 | } 69 | 70 | @Test 71 | public void testResponseConverter(){ 72 | RaftResponseMessage response = new RaftResponseMessage(); 73 | response.setMessageType(this.randomMessageType()); 74 | response.setAccepted(this.random.nextBoolean()); 75 | response.setDestination(this.random.nextInt()); 76 | response.setSource(this.random.nextInt()); 77 | response.setTerm(this.random.nextLong()); 78 | response.setNextIndex(this.random.nextLong()); 79 | 80 | byte[] data = BinaryUtils.messageToBytes(response); 81 | RaftResponseMessage response1 = BinaryUtils.bytesToResponseMessage(data); 82 | assertEquals(response.getMessageType(), response1.getMessageType()); 83 | assertEquals(response.isAccepted(), response1.isAccepted()); 84 | assertEquals(response.getSource(), response1.getSource()); 85 | assertEquals(response.getDestination(), response1.getDestination()); 86 | assertEquals(response.getTerm(), response1.getTerm()); 87 | assertEquals(response.getNextIndex(), response1.getNextIndex()); 88 | } 89 | 90 | @Test 91 | public void testRequestConverter(){ 92 | RaftRequestMessage request = new RaftRequestMessage(); 93 | request.setMessageType(this.randomMessageType());; 94 | request.setCommitIndex(this.random.nextLong()); 95 | request.setDestination(this.random.nextInt()); 96 | request.setLastLogIndex(this.random.nextLong()); 97 | request.setLastLogTerm(this.random.nextLong()); 98 | request.setSource(this.random.nextInt()); 99 | request.setTerm(this.random.nextLong()); 100 | LogEntry[] entries = new LogEntry[this.random.nextInt(20) + 1]; 101 | for(int i = 0; i < entries.length; ++i){ 102 | entries[i] = this.randomLogEntry(); 103 | } 104 | 105 | request.setLogEntries(entries); 106 | byte[] data = BinaryUtils.messageToBytes(request); 107 | byte[] header = new byte[BinaryUtils.RAFT_REQUEST_HEADER_SIZE]; 108 | System.arraycopy(data, 0, header, 0, header.length); 109 | byte[] logData = new byte[data.length - BinaryUtils.RAFT_REQUEST_HEADER_SIZE]; 110 | System.arraycopy(data, BinaryUtils.RAFT_REQUEST_HEADER_SIZE, logData, 0, logData.length); 111 | Pair result = BinaryUtils.bytesToRequestMessage(header); 112 | assertEquals(logData.length, result.getSecond().intValue()); 113 | result.getFirst().setLogEntries(BinaryUtils.bytesToLogEntries(logData)); 114 | assertEquals(request.getMessageType(), result.getFirst().getMessageType()); 115 | assertEquals(request.getCommitIndex(), result.getFirst().getCommitIndex()); 116 | assertEquals(request.getDestination(), result.getFirst().getDestination()); 117 | assertEquals(request.getLastLogIndex(), result.getFirst().getLastLogIndex()); 118 | assertEquals(request.getLastLogTerm(), result.getFirst().getLastLogTerm()); 119 | assertEquals(request.getSource(), result.getFirst().getSource()); 120 | assertEquals(request.getTerm(), result.getFirst().getTerm()); 121 | for(int i = 0; i < entries.length; ++i){ 122 | assertTrue(this.logEntriesEquals(entries[i], result.getFirst().getLogEntries()[i])); 123 | } 124 | } 125 | 126 | private boolean logEntriesEquals(LogEntry entry1, LogEntry entry2){ 127 | boolean equals = entry1.getTerm() == entry2.getTerm() && entry1.getValueType() == entry2.getValueType(); 128 | equals = equals && ((entry1.getValue() != null && entry2.getValue() != null && entry1.getValue().length == entry2.getValue().length) || (entry1.getValue() == null && entry2.getValue() == null)); 129 | if(entry1.getValue() != null){ 130 | int i = 0; 131 | while(equals && i < entry1.getValue().length){ 132 | equals = entry1.getValue()[i] == entry2.getValue()[i]; 133 | ++i; 134 | } 135 | } 136 | 137 | return equals; 138 | } 139 | 140 | private RaftMessageType randomMessageType(){ 141 | byte value = (byte)this.random.nextInt(5); 142 | return RaftMessageType.fromByte((byte) (value + 1)); 143 | } 144 | 145 | private LogEntry randomLogEntry(){ 146 | byte[] value = new byte[this.random.nextInt(20) + 1]; 147 | long term = this.random.nextLong(); 148 | this.random.nextBytes(value); 149 | return new LogEntry(term, value, LogValueType.fromByte((byte)(this.random.nextInt(4) + 1))); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /exts/src/test/java/net/data/technology/jraft/extensions/FileBasedServerStateManagerTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.extensions; 19 | 20 | import static org.junit.Assert.*; 21 | 22 | import java.io.FileOutputStream; 23 | import java.io.IOException; 24 | import java.nio.charset.StandardCharsets; 25 | import java.nio.file.Files; 26 | import java.nio.file.Path; 27 | import java.util.Calendar; 28 | import java.util.Properties; 29 | import java.util.Random; 30 | 31 | import org.junit.Test; 32 | 33 | import com.google.gson.Gson; 34 | import com.google.gson.GsonBuilder; 35 | 36 | import net.data.technology.jraft.ClusterConfiguration; 37 | import net.data.technology.jraft.ClusterServer; 38 | import net.data.technology.jraft.ServerState; 39 | 40 | public class FileBasedServerStateManagerTests { 41 | 42 | private Random random = new Random(Calendar.getInstance().getTimeInMillis()); 43 | 44 | @Test 45 | public void testStateManager() throws IOException{ 46 | Path container = Files.createTempDirectory("logstore"); 47 | Files.deleteIfExists(container.resolve("store.idex")); 48 | Files.deleteIfExists(container.resolve("store.data")); 49 | Files.deleteIfExists(container.resolve("server.state")); 50 | Files.deleteIfExists(container.resolve("config.properties")); 51 | Files.deleteIfExists(container.resolve("cluster.json")); 52 | Files.deleteIfExists(container.resolve("store.sti")); 53 | Files.deleteIfExists(container.resolve("store.idx.bak")); 54 | Files.deleteIfExists(container.resolve("store.sti.bak")); 55 | Files.deleteIfExists(container.resolve("store.data.bak")); 56 | int serverId = this.random.nextInt(); 57 | ClusterConfiguration config = this.randomConfiguration(); 58 | Properties props = new Properties(); 59 | props.put("server.id", String.valueOf(serverId)); 60 | FileOutputStream stream = new FileOutputStream(container.resolve("config.properties").toString()); 61 | props.store(stream, null); 62 | stream.flush(); 63 | stream.close(); 64 | Gson gson = new GsonBuilder().create(); 65 | String data = gson.toJson(config); 66 | stream = new FileOutputStream(container.resolve("cluster.json").toString()); 67 | stream.write(data.getBytes(StandardCharsets.UTF_8)); 68 | stream.flush(); 69 | stream.close(); 70 | FileBasedServerStateManager manager = new FileBasedServerStateManager(container.toString()); 71 | assertTrue(manager.loadLogStore() != null); 72 | assertTrue(manager.readState() == null); 73 | int rounds = 50 + this.random.nextInt(100); 74 | while(rounds > 0){ 75 | ServerState state = new ServerState(); 76 | state.setTerm(this.random.nextLong()); 77 | state.setCommitIndex(this.random.nextLong()); 78 | state.setVotedFor(this.random.nextInt()); 79 | manager.persistState(state); 80 | ServerState state1 = manager.readState(); 81 | assertTrue(state1 != null); 82 | assertEquals(state.getTerm(), state1.getTerm()); 83 | assertEquals(state.getCommitIndex(), state1.getCommitIndex()); 84 | assertEquals(state.getVotedFor(), state1.getVotedFor()); 85 | rounds -= 1; 86 | } 87 | 88 | ClusterConfiguration config1 = manager.loadClusterConfiguration(); 89 | assertConfigEquals(config, config1); 90 | config = this.randomConfiguration(); 91 | manager.saveClusterConfiguration(config); 92 | config1 = manager.loadClusterConfiguration(); 93 | assertConfigEquals(config, config1); 94 | 95 | // clean up 96 | manager.close(); 97 | Files.deleteIfExists(container.resolve("store.idx")); 98 | Files.deleteIfExists(container.resolve("store.data")); 99 | Files.deleteIfExists(container.resolve("store.sti")); 100 | Files.deleteIfExists(container.resolve("store.idx.bak")); 101 | Files.deleteIfExists(container.resolve("store.sti.bak")); 102 | Files.deleteIfExists(container.resolve("store.data.bak")); 103 | Files.deleteIfExists(container.resolve("server.state")); 104 | Files.deleteIfExists(container.resolve("config.properties")); 105 | Files.deleteIfExists(container.resolve("cluster.json")); 106 | Files.deleteIfExists(container); 107 | } 108 | 109 | private ClusterConfiguration randomConfiguration(){ 110 | ClusterConfiguration config = new ClusterConfiguration(); 111 | config.setLastLogIndex(random.nextLong()); 112 | config.setLogIndex(random.nextLong()); 113 | int servers = random.nextInt(10) + 1; 114 | for(int i = 0; i < servers; ++i){ 115 | ClusterServer server = new ClusterServer(); 116 | server.setId(random.nextInt()); 117 | server.setEndpoint(String.format("Server %d", (i + 1))); 118 | config.getServers().add(server); 119 | } 120 | 121 | return config; 122 | } 123 | 124 | private static void assertConfigEquals(ClusterConfiguration config, ClusterConfiguration config1){ 125 | assertEquals(config.getLastLogIndex(), config1.getLastLogIndex()); 126 | assertEquals(config.getLogIndex(), config1.getLogIndex()); 127 | assertEquals(config.getServers().size(), config1.getServers().size()); 128 | for(int i = 0; i < config.getServers().size(); ++i){ 129 | ClusterServer s1 = config.getServers().get(i); 130 | ClusterServer s2 = config.getServers().get(i); 131 | assertEquals(s1.getId(), s2.getId()); 132 | assertEquals(s1.getEndpoint(), s2.getEndpoint()); 133 | } 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /exts/src/test/java/net/data/technology/jraft/extensions/GsonSerializationTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.extensions; 19 | 20 | import static org.junit.Assert.*; 21 | 22 | import java.util.Calendar; 23 | import java.util.Random; 24 | 25 | import org.junit.Test; 26 | 27 | import com.google.gson.Gson; 28 | import com.google.gson.GsonBuilder; 29 | 30 | import net.data.technology.jraft.LogEntry; 31 | import net.data.technology.jraft.LogValueType; 32 | import net.data.technology.jraft.RaftMessageType; 33 | import net.data.technology.jraft.RaftRequestMessage; 34 | import net.data.technology.jraft.RaftResponseMessage; 35 | 36 | public class GsonSerializationTests { 37 | private Random random = new Random(Calendar.getInstance().getTimeInMillis()); 38 | 39 | @Test 40 | public void testResponseSerialization() { 41 | RaftResponseMessage response = new RaftResponseMessage(); 42 | response.setMessageType(this.randomMessageType()); 43 | response.setAccepted(this.random.nextBoolean()); 44 | response.setDestination(this.random.nextInt()); 45 | response.setSource(this.random.nextInt()); 46 | response.setTerm(this.random.nextLong()); 47 | response.setNextIndex(this.random.nextLong()); 48 | 49 | Gson gson = new GsonBuilder().create(); 50 | String payload = gson.toJson(response); 51 | RaftResponseMessage response1 = gson.fromJson(payload, RaftResponseMessage.class); 52 | assertEquals(response.getMessageType(), response1.getMessageType()); 53 | assertEquals(response.isAccepted(), response1.isAccepted()); 54 | assertEquals(response.getSource(), response1.getSource()); 55 | assertEquals(response.getDestination(), response1.getDestination()); 56 | assertEquals(response.getTerm(), response1.getTerm()); 57 | assertEquals(response.getNextIndex(), response1.getNextIndex()); 58 | } 59 | 60 | @Test 61 | public void testRequestSerialization() { 62 | RaftRequestMessage request = new RaftRequestMessage(); 63 | request.setMessageType(this.randomMessageType());; 64 | request.setCommitIndex(this.random.nextLong()); 65 | request.setDestination(this.random.nextInt()); 66 | request.setLastLogIndex(this.random.nextLong()); 67 | request.setLastLogTerm(this.random.nextLong()); 68 | request.setSource(this.random.nextInt()); 69 | request.setTerm(this.random.nextLong()); 70 | LogEntry[] entries = new LogEntry[this.random.nextInt(20) + 1]; 71 | for(int i = 0; i < entries.length; ++i){ 72 | entries[i] = this.randomLogEntry(); 73 | } 74 | 75 | request.setLogEntries(entries); 76 | Gson gson = new GsonBuilder().create(); 77 | String payload = gson.toJson(request); 78 | RaftRequestMessage request1 = gson.fromJson(payload, RaftRequestMessage.class); 79 | assertEquals(request.getMessageType(), request1.getMessageType()); 80 | assertEquals(request.getCommitIndex(), request1.getCommitIndex()); 81 | assertEquals(request.getDestination(), request1.getDestination()); 82 | assertEquals(request.getLastLogIndex(), request1.getLastLogIndex()); 83 | assertEquals(request.getLastLogTerm(), request1.getLastLogTerm()); 84 | assertEquals(request.getSource(), request1.getSource()); 85 | assertEquals(request.getTerm(), request1.getTerm()); 86 | for(int i = 0; i < entries.length; ++i){ 87 | assertTrue(this.logEntriesEquals(entries[i], request1.getLogEntries()[i])); 88 | } 89 | } 90 | 91 | private boolean logEntriesEquals(LogEntry entry1, LogEntry entry2){ 92 | boolean equals = entry1.getTerm() == entry2.getTerm() && entry1.getValueType() == entry2.getValueType(); 93 | equals = equals && ((entry1.getValue() != null && entry2.getValue() != null && entry1.getValue().length == entry2.getValue().length) || (entry1.getValue() == null && entry2.getValue() == null)); 94 | if(entry1.getValue() != null){ 95 | int i = 0; 96 | while(equals && i < entry1.getValue().length){ 97 | equals = entry1.getValue()[i] == entry2.getValue()[i]; 98 | ++i; 99 | } 100 | } 101 | 102 | return equals; 103 | } 104 | 105 | private RaftMessageType randomMessageType(){ 106 | byte value = (byte)this.random.nextInt(5); 107 | return RaftMessageType.fromByte((byte) (value + 1)); 108 | } 109 | 110 | private LogEntry randomLogEntry(){ 111 | byte[] value = new byte[this.random.nextInt(20) + 1]; 112 | long term = this.random.nextLong(); 113 | this.random.nextBytes(value); 114 | return new LogEntry(term, value, LogValueType.fromByte((byte)(this.random.nextInt(4) + 1))); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /exts/src/test/java/net/data/technology/jraft/extensions/H2LogStoreTests.java: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Licensed to the Apache Software Foundation (ASF) under one 4 | * or more contributor license agreements. The ASF licenses 5 | * this file to you under the Apache License, Version 2.0 (the 6 | * "License"); you may not use this file except in compliance 7 | * with the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | package net.data.technology.jraft.extensions; 19 | 20 | import static org.junit.Assert.*; 21 | 22 | import java.io.IOException; 23 | import java.nio.file.Files; 24 | import java.nio.file.Path; 25 | import java.util.Calendar; 26 | import java.util.LinkedList; 27 | import java.util.List; 28 | import java.util.Random; 29 | 30 | import org.junit.Test; 31 | 32 | import net.data.technology.jraft.LogEntry; 33 | import net.data.technology.jraft.LogValueType; 34 | 35 | public class H2LogStoreTests { 36 | 37 | private Random random = new Random(Calendar.getInstance().getTimeInMillis()); 38 | 39 | @Test 40 | public void testPackAndUnpack() throws IOException { 41 | Path container = Files.createTempDirectory("logstore"); 42 | Files.deleteIfExists(container.resolve("test.mv.db")); 43 | Files.deleteIfExists(container.resolve("test.trace.db")); 44 | Path container1 = Files.createTempDirectory("logstore"); 45 | Files.deleteIfExists(container1.resolve("test.mv.db")); 46 | Files.deleteIfExists(container1.resolve("test.trace.db")); 47 | H2LogStore store = new H2LogStore(container.resolve("test").toString()); 48 | H2LogStore store1 = new H2LogStore(container1.resolve("test").toString()); 49 | int logsCount = this.random.nextInt(1000) + 1000; 50 | for(int i = 0; i < logsCount; ++i){ 51 | store.append(this.randomLogEntry()); 52 | store1.append(this.randomLogEntry()); 53 | } 54 | 55 | int logsCopied = 0; 56 | while(logsCopied < logsCount){ 57 | byte[] pack = store.packLog(logsCopied + 1, 100); 58 | store1.applyLogPack(logsCopied + 1, pack); 59 | logsCopied = Math.min(logsCopied + 100, logsCount); 60 | } 61 | 62 | assertEquals(store.getFirstAvailableIndex(), store1.getFirstAvailableIndex()); 63 | for(int i = 1; i <= logsCount; ++i){ 64 | LogEntry entry1 = store.getLogEntryAt(i); 65 | LogEntry entry2 = store1.getLogEntryAt(i); 66 | assertTrue("the " + String.valueOf(i) + "th value are not equal(total: " + String.valueOf(logsCount) + ")", logEntriesEquals(entry1, entry2)); 67 | } 68 | 69 | store.close(); 70 | store1.close(); 71 | 72 | Files.deleteIfExists(container.resolve("test.mv.db")); 73 | Files.deleteIfExists(container.resolve("test.trace.db")); 74 | Files.deleteIfExists(container); 75 | Files.deleteIfExists(container1.resolve("test.mv.db")); 76 | Files.deleteIfExists(container1.resolve("test.trace.db")); 77 | Files.deleteIfExists(container1); 78 | } 79 | 80 | @Test 81 | public void testStore() throws IOException { 82 | Path container = Files.createTempDirectory("logstore"); 83 | Files.deleteIfExists(container.resolve("test.mv.db")); 84 | Files.deleteIfExists(container.resolve("test.trace.db")); 85 | H2LogStore store = new H2LogStore(container.resolve("test").toString()); 86 | assertTrue(store.getLastLogEntry().getTerm() == 0); 87 | assertTrue(store.getLastLogEntry().getValue() == null); 88 | assertEquals(1, store.getFirstAvailableIndex()); 89 | assertTrue(store.getLogEntryAt(1) == null); 90 | 91 | // write some logs 92 | List entries = new LinkedList(); 93 | for(int i = 0; i < this.random.nextInt(100) + 10; ++i){ 94 | LogEntry entry = this.randomLogEntry(); 95 | entries.add(entry); 96 | store.append(entry); 97 | } 98 | 99 | assertEquals(entries.size(), store.getFirstAvailableIndex() - 1); 100 | assertTrue(logEntriesEquals(entries.get(entries.size() - 1), store.getLastLogEntry())); 101 | 102 | // random item 103 | int randomIndex = this.random.nextInt(entries.size()); 104 | assertTrue(logEntriesEquals(entries.get(randomIndex), store.getLogEntryAt(randomIndex + 1))); // log store's index starts from 1 105 | 106 | // random range 107 | randomIndex = this.random.nextInt(entries.size() - 1); 108 | int randomSize = this.random.nextInt(entries.size() - randomIndex); 109 | LogEntry[] logEntries = store.getLogEntries(randomIndex + 1, randomIndex + 1 + randomSize); 110 | for(int i = randomIndex; i < randomIndex + randomSize; ++i){ 111 | assertTrue(logEntriesEquals(entries.get(i), logEntries[i - randomIndex])); 112 | } 113 | 114 | store.close(); 115 | store = new H2LogStore(container.resolve("test").toString()); 116 | 117 | assertEquals(entries.size(), store.getFirstAvailableIndex() - 1); 118 | assertTrue(logEntriesEquals(entries.get(entries.size() - 1), store.getLastLogEntry())); 119 | 120 | // random item 121 | randomIndex = this.random.nextInt(entries.size()); 122 | assertTrue(logEntriesEquals(entries.get(randomIndex), store.getLogEntryAt(randomIndex + 1))); // log store's index starts from 1 123 | 124 | // random range 125 | randomIndex = this.random.nextInt(entries.size() - 1); 126 | randomSize = this.random.nextInt(entries.size() - randomIndex); 127 | logEntries = store.getLogEntries(randomIndex + 1, randomIndex + 1 + randomSize); 128 | for(int i = randomIndex; i < randomIndex + randomSize; ++i){ 129 | assertTrue(logEntriesEquals(entries.get(i), logEntries[i - randomIndex])); 130 | } 131 | 132 | // test with edge 133 | randomSize = this.random.nextInt(entries.size()); 134 | logEntries = store.getLogEntries(store.getFirstAvailableIndex() - randomSize, store.getFirstAvailableIndex()); 135 | for(int i = entries.size() - randomSize, j = 0; i < entries.size(); ++i, ++j){ 136 | assertTrue(logEntriesEquals(entries.get(i), logEntries[j])); 137 | } 138 | 139 | // test write at 140 | LogEntry logEntry = this.randomLogEntry(); 141 | randomIndex = this.random.nextInt((int)store.getFirstAvailableIndex()); 142 | store.writeAt(store.getStartIndex() + randomIndex, logEntry); 143 | assertEquals(randomIndex + 1 + store.getStartIndex(), store.getFirstAvailableIndex()); 144 | assertTrue(logEntriesEquals(logEntry, store.getLastLogEntry())); 145 | 146 | store.close(); 147 | Files.deleteIfExists(container.resolve("test.mv.db")); 148 | Files.deleteIfExists(container.resolve("test.trace.db")); 149 | Files.deleteIfExists(container); 150 | } 151 | 152 | @Test 153 | public void testCompactRandom() throws Exception{ 154 | Path container = Files.createTempDirectory("logstore"); 155 | Files.deleteIfExists(container.resolve("test.mv.db")); 156 | Files.deleteIfExists(container.resolve("test.trace.db")); 157 | H2LogStore store = new H2LogStore(container.resolve("test").toString()); 158 | 159 | // write some logs 160 | List entries = new LinkedList(); 161 | for(int i = 0; i < 300; ++i){ 162 | LogEntry entry = this.randomLogEntry(); 163 | entries.add(entry); 164 | store.append(entry); 165 | } 166 | 167 | long lastLogIndex = entries.size(); 168 | long indexToCompact = this.random.nextInt((int)lastLogIndex - 10) + 1; 169 | store.compact(indexToCompact); 170 | 171 | assertEquals(indexToCompact + 1, store.getStartIndex()); 172 | assertEquals(entries.size(), store.getFirstAvailableIndex() - 1); 173 | 174 | for(int i = 0; i < store.getFirstAvailableIndex() - indexToCompact - 1; ++i){ 175 | LogEntry entry = store.getLogEntryAt(store.getStartIndex() + i); 176 | assertTrue(logEntriesEquals(entries.get(i + (int)indexToCompact), entry)); 177 | } 178 | 179 | int randomIndex = this.random.nextInt((int)(store.getFirstAvailableIndex() - indexToCompact - 1)); 180 | LogEntry logEntry = this.randomLogEntry(); 181 | store.writeAt(store.getStartIndex() + randomIndex, logEntry); 182 | entries.set(randomIndex + (int)indexToCompact, logEntry); 183 | while(entries.size() > randomIndex + (int)indexToCompact + 1){ 184 | entries.remove(randomIndex + (int)indexToCompact + 1); 185 | } 186 | 187 | for(int i = 0; i < store.getFirstAvailableIndex() - indexToCompact - 1; ++i){ 188 | LogEntry entry = store.getLogEntryAt(store.getStartIndex() + i); 189 | assertTrue(logEntriesEquals(entries.get(i + (int)indexToCompact), entry)); 190 | } 191 | 192 | for(int i = 0; i < this.random.nextInt(100) + 10; ++i){ 193 | LogEntry entry = this.randomLogEntry(); 194 | entries.add(entry); 195 | store.append(entry); 196 | } 197 | 198 | for(int i = 0; i < store.getFirstAvailableIndex() - indexToCompact - 1; ++i){ 199 | LogEntry entry = store.getLogEntryAt(store.getStartIndex() + i); 200 | assertTrue(logEntriesEquals(entries.get(i + (int)indexToCompact), entry)); 201 | } 202 | 203 | store.close(); 204 | Files.deleteIfExists(container.resolve("test.mv.db")); 205 | Files.deleteIfExists(container.resolve("test.trace.db")); 206 | Files.deleteIfExists(container); 207 | } 208 | 209 | @Test 210 | public void testCompactAll() throws Exception{ 211 | Path container = Files.createTempDirectory("logstore"); 212 | Files.deleteIfExists(container.resolve("test.mv.db")); 213 | Files.deleteIfExists(container.resolve("test.trace.db")); 214 | H2LogStore store = new H2LogStore(container.resolve("test").toString()); 215 | 216 | // write some logs 217 | List entries = new LinkedList(); 218 | for(int i = 0; i < this.random.nextInt(1000) + 100; ++i){ 219 | LogEntry entry = this.randomLogEntry(); 220 | entries.add(entry); 221 | store.append(entry); 222 | } 223 | 224 | assertEquals(1, store.getStartIndex()); 225 | assertEquals(entries.size(), store.getFirstAvailableIndex() - 1); 226 | assertTrue(logEntriesEquals(entries.get(entries.size() - 1), store.getLastLogEntry())); 227 | long lastLogIndex = entries.size(); 228 | store.compact(lastLogIndex); 229 | 230 | assertEquals(entries.size() + 1, store.getStartIndex()); 231 | assertEquals(entries.size(), store.getFirstAvailableIndex() - 1); 232 | 233 | for(int i = 0; i < this.random.nextInt(100) + 10; ++i){ 234 | LogEntry entry = this.randomLogEntry(); 235 | entries.add(entry); 236 | store.append(entry); 237 | } 238 | 239 | assertEquals(lastLogIndex + 1, store.getStartIndex()); 240 | assertEquals(entries.size(), store.getFirstAvailableIndex() - 1); 241 | assertTrue(logEntriesEquals(entries.get(entries.size() - 1), store.getLastLogEntry())); 242 | 243 | long index = store.getStartIndex() + this.random.nextInt((int)(store.getFirstAvailableIndex() - store.getStartIndex())); 244 | assertTrue(logEntriesEquals(entries.get((int)index - 1), store.getLogEntryAt(index))); 245 | 246 | store.close(); 247 | Files.deleteIfExists(container.resolve("test.mv.db")); 248 | Files.deleteIfExists(container.resolve("test.trace.db")); 249 | Files.deleteIfExists(container); 250 | } 251 | 252 | private LogEntry randomLogEntry(){ 253 | byte[] value = new byte[this.random.nextInt(20) + 1]; 254 | long term = this.random.nextLong(); 255 | this.random.nextBytes(value); 256 | LogValueType type = LogValueType.fromByte((byte)(this.random.nextInt(4) + 1)); 257 | return new LogEntry(term, value, type); 258 | } 259 | 260 | private static boolean logEntriesEquals(LogEntry entry1, LogEntry entry2){ 261 | boolean equals = entry1.getTerm() == entry2.getTerm() && entry1.getValueType() == entry2.getValueType(); 262 | equals = equals && ((entry1.getValue() != null && entry2.getValue() != null && entry1.getValue().length == entry2.getValue().length) || (entry1.getValue() == null && entry2.getValue() == null)); 263 | if(entry1.getValue() != null){ 264 | int i = 0; 265 | while(equals && i < entry1.getValue().length){ 266 | equals = entry1.getValue()[i] == entry2.getValue()[i]; 267 | ++i; 268 | } 269 | } 270 | 271 | return equals; 272 | } 273 | 274 | } 275 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 4 | 4.0.0 5 | 6 | net.data-technology.jraft 7 | jraft-root 8 | pom 9 | 1.0 10 | jraft root project 11 | 12 | 13 | core 14 | exts 15 | dmprinter 16 | 17 | 18 | 19 | 20 | 21 | junit 22 | junit 23 | 4.13.1 24 | 25 | 26 | org.apache.logging.log4j 27 | log4j 28 | 2.13.2 29 | 30 | 31 | com.google.code.gson 32 | gson 33 | 2.6.2 34 | 35 | 36 | org.apache.httpcomponents 37 | httpasyncclient 38 | 4.1.1 39 | 40 | 41 | com.h2database 42 | h2 43 | 2.1.210 44 | 45 | 46 | javax.servlet 47 | javax.servlet-api 48 | 3.1.0 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | org.apache.maven.plugins 58 | maven-compiler-plugin 59 | 60 | 1.8 61 | 1.8 62 | 63 | 64 | 65 | org.apache.maven.plugins 66 | maven-antrun-plugin 67 | 1.1 68 | 69 | 70 | integration-test 71 | 72 | run 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /setup/addsrv.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | if "%*" == "" ( 3 | echo server id is required 4 | goto exit 5 | ) 6 | 7 | set curdir=%~dp0 8 | if exist "%curdir%\server%1" ( 9 | echo server %1 is already created 10 | goto exit 11 | ) 12 | 13 | mkdir "%curdir%\server%1" 14 | echo server.id=%1 > "%curdir%\server%1\config.properties" 15 | echo {"logIndex":0,"lastLogIndex":0,"servers":[{"id": %1,"endpoint": "tcp://localhost:900%1"}]}> "%curdir%\server%1\cluster.json" 16 | start "server%1" /D "%curdir%\server%1" java -jar %curdir%\dmprinter.jar server "%curdir%\server%1" 800%1 17 | @echo on 18 | 19 | :exit -------------------------------------------------------------------------------- /setup/cleanup.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | for /f %%i in ('dir /ad /b') do ( 4 | rmdir /s /q %%i 5 | ) 6 | 7 | if "%1" == "full" del dmprinter.jar 8 | @echo on -------------------------------------------------------------------------------- /setup/init-cluster.json: -------------------------------------------------------------------------------- 1 | { 2 | "logIndex": 0, 3 | "lastLogIndex": 0, 4 | "servers":[ 5 | { 6 | "id": 1, 7 | "endpoint": "tcp://localhost:9001" 8 | }, 9 | { 10 | "id": 2, 11 | "endpoint": "tcp://localhost:9002" 12 | }, 13 | { 14 | "id": 3, 15 | "endpoint": "tcp://localhost:9003" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /setup/setup.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set curdir=%~dp0 3 | if "%1" == "dummy" goto setupDummy 4 | 5 | for %%i in (1 2 3) do ( 6 | mkdir "%curdir%\server%%i" 7 | copy /Y init-cluster.json "%curdir%\server%%i\cluster.json" 8 | echo server.id=%%i> "%curdir%\server%%i\config.properties" 9 | echo start server%%i 10 | start "server%%i" /D "%curdir%\server%%i" java -jar %curdir%\dmprinter.jar server "%curdir%\server%%i" 800%%i 11 | ) 12 | 13 | REM echo start a client 14 | REM mkdir client 15 | REM copy /Y init-cluster.json "%curdir%\client\cluster.json" 16 | REM copy /Y "%curdir%\server1\config.properties" "%curdir%\client\config.properties" 17 | REM start "client" /D "%curdir%\client" java -jar %curdir%\dmprinter.jar client "%curdir%\client" 18 | goto done 19 | :setupDummy 20 | mkdir "%curdir%\dummys" 21 | echo start dummy server 22 | start "Dummy Server" /D "%curdir%\dummys" java -jar %curdir%\dmprinter.jar dummy server 23 | mkdir "%curdir%\dummyc" 24 | echo start dummy client 25 | start "Dummy Client" /D "%curdir%\dummyc" java -jar %curdir%\dmprinter.jar dummy client 26 | :done 27 | @echo on -------------------------------------------------------------------------------- /setup/startsrv.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | set curdir=%~dp0 3 | if "%*" == "" ( 4 | echo server id is required 5 | goto exit 6 | ) 7 | set curdir=%~dp0 8 | if "%1" == "client" ( 9 | if not exist "%curdir%\%1" ( 10 | echo client does not exist 11 | goto exit 12 | ) 13 | 14 | if not "%2" == "" ( 15 | if exist "%curdir%\server%2" ( 16 | COPY /Y "%curdir%\server%2\cluster.json" "%curdir%\%1" 17 | ) 18 | ) 19 | 20 | start "client" /D "%curdir%\%1" java -jar %curdir%\dmprinter.jar client "%curdir%\%1" 21 | goto exit 22 | ) 23 | 24 | if not exist "%curdir%\server%1" ( 25 | echo server %1 is not found 26 | goto exit 27 | ) 28 | 29 | start "server%1" /D "%curdir%\server%1" java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=600%1 -jar %curdir%\dmprinter.jar server "%curdir%\server%1" 800%1 30 | 31 | :exit 32 | @echo on --------------------------------------------------------------------------------