Skip to content

Commit

Permalink
Fix issues in README.md
Browse files Browse the repository at this point in the history
  • Loading branch information
LVala committed Jul 26, 2024
1 parent 03af451 commit 5c89bb1
Showing 1 changed file with 31 additions and 33 deletions.
64 changes: 31 additions & 33 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,20 @@

# `zaft`

![Zig version](https://img.shields.io/badge/zig-0.13.0-orange?style=flat&logo=zig&label=Zig&color=%23eba742)
![Release](https://img.shields.io/github/v/release/LVala/zaft)
![CI](https://img.shields.io/github/actions/workflow/status/LVala/zaft/ci.yml)
<a href="https://ziglang.org/download/"><img src="https://img.shields.io/badge/0.13.0-orange?style=flat&logo=zig&label=Zig&color=%23eba742"></a>
<a href="https://github.com/LVala/zaft/releases"><img src="https://img.shields.io/github/v/release/LVala/zaft?link=https%3A%2F%2Fgithub.com%2FLVala%2Fzaft%2Freleases"></a>
<a href="https://github.com/LVala/zaft/actions"><img src="https://img.shields.io/github/actions/workflow/status/LVala/zaft/ci.yml?link=https%3A%2F%2Fgithub.com%2FLVala%2Fzaft%2Factions"></a>

### The Raft Consensus Algorithm in Zig

This repository houses `zaft` - [Raft Consensus Algorithm](https://raft.github.io/) library implemented in Zig. It provides the building blocks
for creating distributed systems requiring consensus among replicated state machines, like databases.


![tang demo](tang.gif)
</div>

## Installation

This package can be installed using the Zig package manager. In your `build.zig.zon` add `zaft` to dependency list:
This package can be installed using the Zig package manager. In your `build.zig.zon` add `zaft` to the dependency list:

```zig
// in build.zig.zon
Expand All @@ -35,7 +33,7 @@ This package can be installed using the Zig package manager. In your `build.zig.

Output of `zig build` will provide you with valid hash, use it to replace the one above.

Finally, add the `zaft` module in you `build.zig`:
Add the `zaft` module in you `build.zig`:

```zig
// in build.zig
Expand All @@ -51,41 +49,41 @@ const zaft = @import("zaft");

## Usage

This section will show you how to integrate `zaft` with your program step-by-step. If you want to take look at a fully working example,
This section will show you how to integrate `zaft` with your program step-by-step. If you prefer to take a look at a fully working example,
check out the [kv_store](./examples/kv_store) - in-memory, replicated key-value store based on `zaft`.

> [!IMPORTANT]
> This tutorial assumes some familiarity with the Raft Consensus Algorithm. If not, I highly advise you to at least skim through
> the [Raft paper](https://raft.github.io/raft.pdf). Don't worry, it's a short and very well written paper!
> the [Raft paper](https://raft.github.io/raft.pdf). Don't worry, it's a short and very well-written paper!
Firstly, initialise the `Raft` struct:
Firstly, initialize the `Raft` struct:

```zig
// we'll get to UserData and Entry in a second
const Raft = @import("zaft").Raft(UserData, Entry);
const raft = Raft.init(config, initial_state, callbacks);
const raft = Raft.init(config, initial_state, callbacks, allocator);
defer raft.deinit();
```

`Raft.init` takes three arguments:
`Raft.init` takes four arguments:

* `config` - configuration of this particular Raft node:

```zig
const config = Raft.Config{
.id = 3, // id of this Raft node
.server_no = 5, // total number of Raft nodes
.server_no = 5, // total number of Raft nodes, there should be nodes with ids from 0 up to server_no-1
// there's other options with sane defaults, check out this struct's definition to learn
// what else you can configure
// what else can be configured
};
```

* `callbacks` - `Raft` will call this function to perform various actions:

```zig
// makeRPC is used to send Raft messages to other nodes
// this function should be non-blocking, that is, not wait for the response
// this function should be non-blocking (not wait for the response)
fn makeRPC(ud: *UserData, id: u32, rpc: Raft.RPC) !void {
const address = ud.node_addresse[id];
// it's your responsibility to serialize the message, consider using e.g. std.json
Expand All @@ -96,7 +94,7 @@ fn makeRPC(ud: *UserData, id: u32, rpc: Raft.RPC) !void {
// Entry can be whatever you want
// in this callback the entry should be applied to the state machine
// applying an entry must be deterministic! This means that, after applying the
// same entries in the same order, the state machine must be in the same state every time
// same entries in the same order, the state machine must end up in the same state every time
fn applyEntry(ud: *UserData, entry: Entry) !void {
// let's assume that is some kind of key-value store
switch(entry) {
Expand All @@ -105,30 +103,28 @@ fn applyEntry(ud: *UserData, entry: Entry) !void {
}
}
// this function needs to persist a new log entry
// these two function have to append/pop entry to/from the log
fn logAppend(ud: *UserData, log_entry: Raft.LogEntry) !void {
try ud.database.appendEntry(log_entry);
}
// this function needs to pop the last log entry from the persistent storage
fn logPop(ud: *UserData) !Raft.LogEntry {
const log_entry = try ud.database.popEntry();
return log_entry;
}
// this function needs to persist current_term
// these function are responsible for persisting values, that can be overriden on every save
fn persistCurrentTerm(ud: *UserData, current_term: u32) !void {
try ud.database.persistCurrentTerm(current_term);
}
// this function needs to persist voted_for
fn persistVotedFor(ud: *UserData, voted_for: ?u32) !void {
try ud.database.persistVotedFor(voted_for);
}
```

> [!WARNING]
> Notice that all of the callbacks can return an error (mostly for the sake of convinience).
> Notice that all of the callbacks can return an error (mostly for the sake of convenience).
>
> Error returned from `makeRPC` will be ignored, the RPC will be simply retried after
> an appropriate timeout. Errors returned from other function, as of now, will result in a panic.
Expand Down Expand Up @@ -156,8 +152,8 @@ const callbacks = Raft.Callbacks {
};
```

* `initial_state` - the persisted state of this Raft node. On each reboot, you need to read the persisted Raft state, that
is the `current_term`, `voted_for` and `log` and use it as the `InitialState`:
* `initial_state` - the persisted state of this Raft node. On each reboot, you need to read the persisted Raft state
(`current_term`, `voted_for`, and `log`, previouslt saved by the callbacks) and use it as the `InitialState`:

```zig
const initial_state = Raft.InitialState {
Expand All @@ -168,23 +164,25 @@ const initial_state = Raft.InitialState {
};
```

Lastly, an `std.mem.Allocator` will be used to provide memory for the Raft log.

---

The `Raft` struct needs to be periodically ticked in order to trigger timeouts and other necessary actions. You can use a separate thread to do that, or
built your app based on an event loop like [libexev](https://github.com/mitchellh/libxev) with its `xev.Timer`.
The `Raft` struct needs to be periodically ticked to trigger timeouts and other necessary actions. You can use a separate thread to do that, or
built your program based on an event loop like [libexev](https://github.com/mitchellh/libxev) with its `xev.Timer`.

```zig
const tick_after = raft.tick();
// tick_after is a number of milliseconds after which raft should be ticked again
// tick_after is the number of milliseconds after which raft should be ticked again
```

For instance, [kv_store](./examples/kv_store/src/ticker.zig) uses a separate thread exclusively to tick the `Raft` struct.

> [!WARNING]
> The `Raft` struct is *not* thread-safe. Use appropriate synchronization means to makes sure it is not accessed simultaneously by many threads
> The `Raft` struct is *not* thread-safe. Use appropriate synchronization means to make sure it is not accessed simultaneously by many threads
> (e.g. a simple `std.Thread.Mutex` will do).
Next, messages from other Raft nodes need to be feed to local `Raft` struct by calling:
Next, messages from other Raft nodes need to be fed to the local `Raft` struct by calling:

```zig
// you will need to receive and deserialize the messages from other peers
Expand All @@ -199,18 +197,18 @@ const entry = Entry { ... };
const idx = try raft.appendEntry(entry);
```

It will return an index of the new entry. According to the Raft algorithm, you application should block on client request
until the entry has been applied. You can use `std.Thread.Condition` and call its `notify` function in the `applyEntry` callback in order to notify
the application that the entry was applied. You can check whether entry was applied by using `raft.checkIfApplied(idx)`.
It will return an index of the new entry. According to the Raft algorithm, your program should block on client requests
until the entry has been applied. You can use `std.Thread.Condition` and call its `notify` function in the `applyEntry` callback to notify
the program that the entry was applied. You can check whether the entry was applied by using `raft.checkIfApplied(idx)`.
Take a look at how [kv_store](./examples/kv_store/src/main.zig) does this.

`appendEntry` function will return error if the node is not a leader. In such case, you should redirect the client request to the leader node.
`appendEntry` function will return an error if the node is not a leader. In such a case, you should redirect the client request to the leader node.
You can check which node is the leader by using `raft.getCurrentLeader()`. You can also check if the node is a leader proactively by calling
`raft.checkifLeader()`.

## Next steps

The library already can be used, but it's missing some of the Raft Algorithm, or other features, like:
The library already can be successfully used, but there's plenty of room for improvements and new features, like:

* Creating a simulator to test and find problems in the implementation.
* Add auto-generated API documentation based on `zig build -femit-docs`.
Expand Down

0 comments on commit 5c89bb1

Please sign in to comment.