From 29623be2d75288b3d8d30653112973a7fd364904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wala?= Date: Sat, 20 Jul 2024 15:53:40 +0200 Subject: [PATCH] test --- README.md | 160 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 159 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c1bd56a..b5ad277 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,169 @@ ![GitHub Version](https://img.shields.io/github/v/release/LVala/tang) ![GitHub License](https://img.shields.io/github/license/LVala/tang) -### Implementation of the [Raft Consensus Algorithm](https://raft.github.io/) in Zig +### The Raft Consensus Algorithm in Zig + +This repository houses `zaft` - a [Raft Consensus Algorithm](https://raft.github.io/) library implemented in Zig. It provides the building blocks +for building distributed systems requiring consensus among replicated state machines, like databases. + ![tang demo](tang.gif) ## Installation +This package can be installed using the Zig package manager. In your `build.zig.zon` file add `zaft` to dependency list: + +```zig +// build.zig.zon +.{ + .name = "my-project", + .version = "0.0.0", + .dependencies = .{ + .zaft = .{ + .url = + }, + }, +} +``` + +Finally, add the `zaft` module in you `build.zig`: + +```zig +const zaft = b.dependency("zaft", .{ .target = target, .optimize = optimize }); +exe.root_module.addImport("zaft", zaft.module("zaft")); +``` + +Now you should be able to import `zaft` in your `exe`s root source file: + +```zig +const zaft = @import("zaft"); +``` + ## Usage + +> [!IMPORTANT] +> This usage 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) and familiarize yourself with how the algorithm works. Don't worry, it's a very well +> written paper! + +Firstly, initialise the `Raft` struct + +```zig +// we'll get through to UserData and Entry later +const Raft = @import("zaft").Raft(UserData, Entry); + +const config = Raft.Config{ +}; +const callbacks = Raft.Callbacks{ + // .. +}; +const initial_state = Raft.InitialState{ + // ... +}; + +const raft = Raft.init(config, initial_state, callbacks) +``` + +`Raft.init` takes three 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 + // other options with sane defaults, check out this struct's definition to learn + // what else you can configure +}; +``` + +* `callbacks` - `Raft` will call this function to perform various actions: + +```zig +fn makeRPC(ud: *UserData, id: u32, rpc: Raft.RPC) !void { + // makeRPC is used to send Raft messages to other nodes + // this function should be non-blocking, that is, not wait for the response + const address = ud.node_addresse[id]; + // it's your responsibility to serialize the message, consider using e.g. std.json + const msg: []u8 = serialize(rpc); + try ud.client.send(address, msg); +} + +fn applyEntry(ud: *UserData, entry: Entry) !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! That is, after applying the + // same entries in the same order, the state machine must be in the same state + + // let's assume that is some kind of key-value store + switch(entry) { + .add => |add| try ud.store.add(add.key, add.value), + .remove => |remove| try ud.store.remove(remove.key), + } +} + +fn logAppend(ud: *UserData, log_entry: Raft.LogEntry) !void { + // this function needs to persist a new log entry + try ud.database.appendEntry(log_entry); +} + +fn logPop(ud: *UserData) !Raft.LogEntry { + // this function needs to pop the last log entry from the persistent storage + const log_entry = try ud.database.popEntry(); + return log_entry; +} + +fn persistCurrentTerm(ud: *UserData, current_term: u32) !void { + // this function needs to persist current_term + try ud.database.persistCurrentTerm(current_term); +} + + +fn persistVotedFor(ud: *UserData, voted_for: ?u32) !void { + // this function needs to persist voted_for + try ud.database.persistVotedFor(voted_for); +} +``` + +> [!WARNING] +> Notice that all of the callbacks can return an error for the sake of convinience. +> Error returned from `makeRPC` will be ignored, the RPC will be simply retried after +> appropriate timeou. Error returned from other function, as of now, will result in panic. + +```zig +// pointer to user_data will be passed as a first argument to all of the callbacks +// you can place whatever you want as the UserData +const user_data = UserData { + database: Database, + http_client: HttpClient, + node_addresses: []std.net.Address, + store: Store, +}; + +const callbacks = Raft.Callbacks { + .user_data = &user_data, + .makeRPC = makeRPC, + .applyEntry = applyEntry, + .logAppend = logAppend, + .logPop = logPop, + .persistCurrentTerm = persisCurrentTerm, + .persistVotedFor = persistVotedFor, +}; +``` + +* `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`: + +```zig +const initial_state = Raft.InitialState { + // lets assume we saved the state to a + .voted_for = user_data.database.readVotedFor(), + .current_term = user_data.database.readCurrentTerm(), + .log = user_data.database.readLog(), +}; +``` + + + +