This project was developed during (and after) David Beazley's Raft course. It's an implementation of the Raft consensus algorithm in TypeScript.
Right now it likely contains some bugs and will not be performant. Verifying that the program works correctly will require extra work.
This implementation does not support configuration changes.
The library only supports running Raft nodes on a single machine.
To support multiple machines, the Raft class should take urls and not ports as inputs.
A Raft instance contains all core raft logic around leader election, log replication, but also persistence, timers and communicating with other raft instances. You pass it a state machine (e.g. a key-value store) and some configuration. The instance will then automatically connect to other instances, and replicate the log.
const raft = new Raft<LogValueType>(
nodePort,
otherNodePorts,
stateMachine,
logger,
persistenceFilePath,
slowdownTimeBy,
leaderElectionTimeoutMs,
heartbeatTimeoutMs,
);nodePort: numberthe port on which to listen for incoming connections from other Raft instances.otherNodePorts: Array<number>the ports of other raft nodes in the cluster.stateMachine: StateMachinethe state machine (see below).logger: bunyan.Loggera logger, which in the future shouldn't be a dependency logger but an interface.persistenceFilePath: stringpath to the file where the persistent data from this raft node should be read and written.slowdownTimeBy: number | undefinedslow down time for testing. Default: 1.leaderElectionTimeoutMs: number | undefinedraft will wait for[t, t * 2]to call a new leader election. Default: 3000.heartbeatTimeoutMs: number | undefinedthe time after which leaders send heartbeats to followers. Default: 500.
Append a value to the log. The raft instance that this is called on should be the leader. The raft instance will apply the value to the state machine when it is committed.
logValue: LogValueTypethe value that is applied to the state machine (see below) when committed (replicated across a majority).requestId: { clientId: number, requestSerial: number }unique identifier of this request. When retrying requests, use the same identifier.requestIdshould be monotonically increasing for different requests.- Returns:
Promise<either.Either<'notLeader' | 'timedOut', undefined>>- Resolves with
either.right(undefined)when the entry is committed. - Resolves with
either.left('notLeader')when the raft instance is not the leader. - Resolves with
either.left('timedOut')if the request timed out. This can happen on network partitions. Retry with the same request ID.
- Resolves with
Should be called before every read on the state machine. See section 8 of the Raft paper on details what this function does.
- Returns:
Promise<{ isLeader: boolean }>- Resolves with
{ isLeader: true }when it is safe to read from the state machine. - Resolves with
{ isLeader: false }when the raft instance is not the leader. After this, it is not safe to read from the state machine.
- Resolves with
type StateMachine<LogValueType> = {
handleValue(value: LogValueType): void;
};Raft requires the state machine to implement handleValue, which the raft instance calls with the values of committed log entries.
The default implementation is a simple key value server. Its state machine handles values of type:
type KeyValueStoreAction =
| {
type: 'set';
key: string;
value: string;
}
| {
type: 'delete';
key: string;
};These values are appended to the log by the http handlers in src/index.ts.
Run yarn in the root directory.
Running the server can be done with the command
$ yarn serverIt accepts the following environment variables:
PORTthe port on which to run the http handler. E.g.3000OTHER_PORTSother ports in the cluster. E.g.3001,3002PERSISTENCE_FILE_PATHpath to file in which raft stores persistent data.LOG_LEVELinfo or debug log level. Default: info.
Running a client can be done with the command
$ yarn client $CLIENT_IDIt starts an interactive shell on which to type commands. Supported are:
get KEY PORT REQUEST_IDset KEY VALUE PORT REQUEST_IDdelete KEY PORT REQUEST_ID
The parameters are as follows:
KEY: the key of the entry in the key value store.VALUEthe value of the entry in the key value store.PORTthe port of the server on which to send the request.REQUEST_IDthe id of the request. Default: monotonically increasing request id.