From 5c89bb10ac594d8a84a4cb03c79782b4e9f168ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Wala?= Date: Fri, 26 Jul 2024 20:26:13 +0200 Subject: [PATCH] Fix issues in README.md --- README.md | 64 +++++++++++++++++++++++++++---------------------------- 1 file changed, 31 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 24906b1..9320485 100644 --- a/README.md +++ b/README.md @@ -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) + + + ### 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) ## 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 @@ -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 @@ -51,33 +49,33 @@ 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 }; ``` @@ -85,7 +83,7 @@ const config = Raft.Config{ ```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 @@ -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) { @@ -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. @@ -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 { @@ -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 @@ -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`.