Version 0.8

This commit is contained in:
2026-02-19 15:27:47 +01:00
commit 868312edf8
37 changed files with 12782 additions and 0 deletions
+225
View File
@@ -0,0 +1,225 @@
#if defined(MAIN_SIMULATION) || defined(MAIN_TEST)
#define QUAKEY_ENABLE_MOCKS
#endif
#include <quakey.h>
#include <stdint.h>
#include <assert.h>
#include "node.h"
#include "client.h"
#define CLIENT_TRACE(fmt, ...) {}
//#define CLIENT_TRACE(fmt, ...) fprintf(stderr, "CLIENT: " fmt "\n", ##__VA_ARGS__);
#define CLIENT_REQUEST_TIMEOUT_SEC 3
static uint64_t next_client_id = 1;
static int
process_message(ClientState *state,
int conn_idx, uint8_t type, ByteView msg)
{
(void) conn_idx;
if (type == MESSAGE_TYPE_REDIRECT) {
RedirectMessage redirect_message;
if (msg.len != sizeof(RedirectMessage))
return -1;
memcpy(&redirect_message, msg.ptr, sizeof(redirect_message));
if (redirect_message.leader_idx >= 0 && redirect_message.leader_idx < state->num_servers) {
CLIENT_TRACE("Redirected to leader %d", redirect_message.leader_idx);
state->current_leader = redirect_message.leader_idx;
// Retry immediately with the correct leader
state->pending = false;
}
return 0;
}
if (!state->pending)
return 0;
if (type != MESSAGE_TYPE_REPLY)
return 0;
ReplyMessage reply_message;
if (msg.len != sizeof(ReplyMessage))
return -1;
memcpy(&reply_message, msg.ptr, sizeof(reply_message));
CLIENT_TRACE("Received reply for request %lu", (unsigned long)state->request_id);
state->pending = false;
return 0;
}
int client_init(void *state_, int argc, char **argv,
void **ctxs, struct pollfd *pdata, int pcap, int *pnum,
int *timeout)
{
ClientState *state = state_;
state->num_servers = 0;
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "--server")) {
i++;
if (i == argc) {
fprintf(stderr, "Option --server missing value. Usage is --server <addr>:<port>\n");
return -1;
}
if (state->num_servers == NODE_LIMIT) {
fprintf(stderr, "Node limit of %d reached\n", NODE_LIMIT);
return -1;
}
if (parse_addr_arg(argv[i], &state->server_addrs[state->num_servers++]) < 0) {
fprintf(stderr, "Malformed <addr>:<port> pair for --server option\n");
return -1;
}
} else {
printf("Ignoring option '%s'\n", argv[i]);
}
}
// Now sort the addresses
addr_sort(state->server_addrs, state->num_servers);
if (tcp_context_init(&state->tcp) < 0) {
fprintf(stderr, "Client :: Couldn't setup TCP context\n");
return -1;
}
state->pending = false;
state->client_id = next_client_id++;
state->request_id = 0;
state->current_leader = 0;
Time now = get_current_time();
if (now == INVALID_TIME) {
fprintf(stderr, "Client :: Couldn't get current time\n");
tcp_context_free(&state->tcp);
return -1;
}
state->last_request_time = now;
// Connect to all known servers
for (int i = 0; i < state->num_servers; i++) {
if (tcp_connect(&state->tcp, state->server_addrs[i], i, NULL) < 0) {
fprintf(stderr, "Client :: Couldn't connect to server %d\n", i);
tcp_context_free(&state->tcp);
return -1;
}
}
*timeout = 0;
if (pcap < TCP_POLL_CAPACITY) {
fprintf(stderr, "Client :: Not enough poll() capacity (got %d, needed %d)\n", pcap, TCP_POLL_CAPACITY);
return -1;
}
*pnum = tcp_register_events(&state->tcp, ctxs, pdata);
return 0;
}
int client_tick(void *state_, void **ctxs,
struct pollfd *pdata, int pcap, int *pnum, int *timeout)
{
ClientState *state = state_;
Time now = get_current_time();
if (now == INVALID_TIME) {
assert(0);
}
Event events[TCP_EVENT_CAPACITY];
int num_events = tcp_translate_events(&state->tcp, events, ctxs, pdata, *pnum);
for (int i = 0; i < num_events; i++) {
if (events[i].type != EVENT_MESSAGE)
continue;
int conn_idx = events[i].conn_idx;
for (;;) {
ByteView msg;
uint16_t msg_type;
int ret = tcp_next_message(&state->tcp, conn_idx, &msg, &msg_type);
if (ret == 0)
break;
if (ret < 0) {
tcp_close(&state->tcp, conn_idx);
break;
}
ret = process_message(state, conn_idx, msg_type, msg);
if (ret < 0) {
tcp_close(&state->tcp, conn_idx);
break;
}
tcp_consume_message(&state->tcp, conn_idx);
}
}
// Timeout: if pending request has no response, try next server
if (state->pending) {
Time request_deadline = state->last_request_time + CLIENT_REQUEST_TIMEOUT_SEC * 1000000000ULL;
if (now >= request_deadline) {
CLIENT_TRACE("Request %lu timed out, trying next server",
(unsigned long)state->request_id);
state->pending = false;
state->current_leader = (state->current_leader + 1) % state->num_servers;
}
}
// Send a new request if not currently waiting for a response
if (!state->pending) {
int leader = state->current_leader;
int conn_idx = tcp_index_from_tag(&state->tcp, leader);
if (conn_idx < 0) {
// Connection lost, try reconnecting
tcp_connect(&state->tcp, state->server_addrs[leader], leader, NULL);
} else {
state->request_id++;
RequestMessage request_message = {
.base = {
.version = MESSAGE_VERSION,
.type = MESSAGE_TYPE_REQUEST,
.length = sizeof(RequestMessage),
},
.oper = OPERATION_A,
.client_id = state->client_id,
.request_id = state->request_id,
};
ByteQueue *output = tcp_output_buffer(&state->tcp, conn_idx);
if (output)
byte_queue_write(output, &request_message, sizeof(request_message));
state->pending = true;
state->last_request_time = now;
}
}
// Set timeout for next tick
if (state->pending) {
Time request_deadline = state->last_request_time + CLIENT_REQUEST_TIMEOUT_SEC * 1000000000ULL;
*timeout = deadline_to_timeout(request_deadline, now);
} else {
*timeout = 0; // Send next request immediately
}
if (pcap < TCP_POLL_CAPACITY)
return -1;
*pnum = tcp_register_events(&state->tcp, ctxs, pdata);
return 0;
}
int client_free(void *state_)
{
ClientState *state = state_;
tcp_context_free(&state->tcp);
return 0;
}
+39
View File
@@ -0,0 +1,39 @@
#ifndef CLIENT_INCLUDED
#define CLIENT_INCLUDED
#include <lib/tcp.h>
#include <lib/basic.h>
#include "config.h"
typedef struct {
TCP tcp;
// True if we are waiting for a response
bool pending;
Address server_addrs[NODE_LIMIT];
int num_servers;
uint64_t client_id;
uint64_t request_id;
int current_leader;
Time last_request_time;
} ClientState;
struct pollfd;
int client_init(void *state, int argc, char **argv,
void **ctxs, struct pollfd *pdata, int pcap, int *pnum,
int *timeout);
int client_tick(void *state, void **ctxs,
struct pollfd *pdata, int pcap, int *pnum, int *timeout);
int client_free(void *state);
#endif // CLIENT_INCLUDED
+8
View File
@@ -0,0 +1,8 @@
#ifndef CONFIG_INCLUDED
#define CONFIG_INCLUDED
#define NODE_LIMIT 32
#define HEARTBEAT_INTERVAL_SEC 1
#define PRIMARY_DEATH_TIMEOUT_SEC 2
#endif // CONFIG_INCLUDED
+234
View File
@@ -0,0 +1,234 @@
#ifdef MAIN_SIMULATION
#define QUAKEY_ENABLE_MOCKS
#include <signal.h>
#include <stdint.h>
#include <string.h>
#include <quakey.h>
#include "node.h"
#include "client.h"
static volatile int simulation_running = 1;
static void sigint_handler(int sig)
{
(void)sig;
simulation_running = 0;
}
int main(void)
{
signal(SIGINT, sigint_handler);
Quakey *quakey;
int ret = quakey_init(&quakey, 2);
if (ret < 0)
return -1;
// Client 1
{
QuakeySpawn config = {
.name = "rndcli1",
.state_size = sizeof(ClientState),
.init_func = client_init,
.tick_func = client_tick,
.free_func = client_free,
.addrs = (char*[]) { "127.0.0.2" },
.num_addrs = 1,
.disk_size = 10<<20,
.platform = QUAKEY_LINUX,
};
quakey_spawn(quakey, config, "cli --server 127.0.0.4:8080 --server 127.0.0.5:8080 --server 127.0.0.6:8080");
}
// Client 2
{
QuakeySpawn config = {
.name = "rndcli2",
.state_size = sizeof(ClientState),
.init_func = client_init,
.tick_func = client_tick,
.free_func = client_free,
.addrs = (char*[]) { "127.0.0.3" },
.num_addrs = 1,
.disk_size = 10<<20,
.platform = QUAKEY_LINUX,
};
quakey_spawn(quakey, config, "cli --server 127.0.0.4:8080 --server 127.0.0.5:8080 --server 127.0.0.6:8080");
}
// Node 1
{
QuakeySpawn config = {
.name = "node1",
.state_size = sizeof(NodeState),
.init_func = node_init,
.tick_func = node_tick,
.free_func = node_free,
.addrs = (char*[]) { "127.0.0.4" },
.num_addrs = 1,
.disk_size = 10<<20,
.platform = QUAKEY_LINUX,
};
quakey_spawn(quakey, config, "nd --addr 127.0.0.4:8080 --peer 127.0.0.5:8080 --peer 127.0.0.6:8080");
}
// Node 2
{
QuakeySpawn config = {
.name = "node2",
.state_size = sizeof(NodeState),
.init_func = node_init,
.tick_func = node_tick,
.free_func = node_free,
.addrs = (char*[]) { "127.0.0.5" },
.num_addrs = 1,
.disk_size = 10<<20,
.platform = QUAKEY_LINUX,
};
quakey_spawn(quakey, config, "nd --peer 127.0.0.4:8080 --addr 127.0.0.5:8080 --peer 127.0.0.6:8080");
}
// Node 3
{
QuakeySpawn config = {
.name = "node3",
.state_size = sizeof(NodeState),
.init_func = node_init,
.tick_func = node_tick,
.free_func = node_free,
.addrs = (char*[]) { "127.0.0.6" },
.num_addrs = 1,
.disk_size = 10<<20,
.platform = QUAKEY_LINUX,
};
quakey_spawn(quakey, config, "nd --peer 127.0.0.4:8080 --peer 127.0.0.5:8080 --addr 127.0.0.6:8080");
}
while (simulation_running)
quakey_schedule_one(quakey);
quakey_free(quakey);
return 0;
}
#endif // MAIN_SIMULATION
////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////
#ifdef MAIN_CLIENT
#include <poll.h>
#include "client.h"
#define POLL_CAPACITY 1024
int main(int argc, char **argv)
{
int ret;
ClientState state;
void* poll_ctxs[POLL_CAPACITY];
struct pollfd poll_array[POLL_CAPACITY];
int poll_count;
int poll_timeout;
ret = client_init(
&state,
argc,
argv,
poll_ctxs,
poll_array,
POLL_CAPACITY,
&poll_count,
&poll_timeout
);
if (ret < 0)
return -1;
for (;;) {
#ifdef _WIN32
WSAPoll(poll_array, poll_count, poll_timeout);
#else
poll(poll_array, poll_count, poll_timeout);
#endif
ret = client_tick(
&state,
poll_ctxs,
poll_array,
POLL_CAPACITY,
&poll_count,
&poll_timeout
);
if (ret < 0)
return -1;
}
client_free(&state);
return 0;
}
#endif // MAIN_CLIENT
////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////
#ifdef MAIN_SERVER
#include <poll.h>
#include "node.h"
#define POLL_CAPACITY 1024
int main(int argc, char **argv)
{
int ret;
NodeState state;
void* poll_ctxs[POLL_CAPACITY];
struct pollfd poll_array[POLL_CAPACITY];
int poll_count;
int poll_timeout;
ret = node_init(
&state,
argc,
argv,
poll_ctxs,
poll_array,
POLL_CAPACITY,
&poll_count,
&poll_timeout
);
if (ret < 0)
return -1;
for (;;) {
#ifdef _WIN32
WSAPoll(poll_array, poll_count, poll_timeout);
#else
poll(poll_array, poll_count, poll_timeout);
#endif
ret = node_tick(
&state,
poll_ctxs,
poll_array,
POLL_CAPACITY,
&poll_count,
&poll_timeout
);
if (ret < 0)
return -1;
}
node_free(&state);
return 0;
}
#endif // MAIN_SERVER
+1265
View File
File diff suppressed because it is too large Load Diff
+146
View File
@@ -0,0 +1,146 @@
#ifndef NODE_INCLUDED
#define NODE_INCLUDED
#include <lib/tcp.h>
#include <lib/basic.h>
#include <lib/message.h>
#include "wal.h"
#include "config.h"
enum {
MESSAGE_TYPE_REQUEST_VOTE,
MESSAGE_TYPE_VOTED,
MESSAGE_TYPE_APPEND_ENTRIES,
MESSAGE_TYPE_APPENDED,
MESSAGE_TYPE_REQUEST,
MESSAGE_TYPE_REPLY,
MESSAGE_TYPE_REDIRECT,
};
typedef struct {
MessageHeader base;
uint64_t term;
int sender_idx;
int last_log_index;
uint64_t last_log_term;
} RequestVoteMessage;
typedef struct {
MessageHeader base;
uint64_t term;
uint8_t value;
} VotedMessage;
typedef struct {
MessageHeader base;
uint64_t term;
int leader_idx;
int prev_log_index;
uint64_t prev_log_term;
int leader_commit;
int entry_count;
// Followed by: LogEntry entries[entry_count]
} AppendEntriesMessage;
typedef struct {
MessageHeader base;
int sender_idx;
uint64_t term;
uint8_t success;
int match_index;
} AppendedMessage;
typedef struct {
MessageHeader base;
Operation oper;
uint64_t client_id;
uint64_t request_id;
} RequestMessage;
typedef struct {
MessageHeader base;
OperationResult result;
} ReplyMessage;
typedef struct {
MessageHeader base;
int leader_idx;
} RedirectMessage;
typedef struct {
uint64_t client_id;
uint64_t last_request_id;
OperationResult last_result;
bool pending;
int conn_tag;
} ClientTableEntry;
typedef struct {
int count;
int capacity;
ClientTableEntry *entries;
} ClientTable;
typedef enum {
ROLE_FOLLOWER,
ROLE_CANDIDATE,
ROLE_LEADER,
} Role;
typedef struct {
TCP tcp;
WAL wal;
Address self_addr;
Address node_addrs[NODE_LIMIT];
int num_nodes;
Role role;
uint64_t term;
int voted_for;
Handle term_and_vote_handle;
// Index of the current leader
int leader_idx;
int commit_index;
int last_applied;
// When CANDIDATE
int votes_received;
// When LEADER
int next_indices[NODE_LIMIT];
int match_indices[NODE_LIMIT];
// Relative timeout in nanoseconds
uint64_t election_timeout;
// Last heartbeat time
uint64_t watchdog;
// Keep track of client request order to enforce
// linearizability
ClientTable client_table;
int next_client_tag;
// The state machine to be replicated
StateMachine state_machine;
} NodeState;
struct pollfd;
int node_init(void *state, int argc, char **argv,
void **ctxs, struct pollfd *pdata, int pcap, int *pnum,
int *timeout);
int node_tick(void *state, void **ctxs,
struct pollfd *pdata, int pcap, int *pnum, int *timeout);
int node_free(void *state);
#endif // NODE_INCLUDED
+127
View File
@@ -0,0 +1,127 @@
#if defined(MAIN_SIMULATION) || defined(MAIN_TEST)
#define QUAKEY_ENABLE_MOCKS
#endif
#include <quakey.h>
#include <assert.h>
#include <stdlib.h>
#include "wal.h"
int wal_init(WAL *wal, string file)
{
Handle handle;
if (file_open(file, &handle) < 0)
return -1;
wal->handle = handle;
size_t size;
if (file_size(handle, &size) < 0) {
file_close(handle);
return -1;
}
if (size % sizeof(WALEntry) != 0) {
file_close(handle);
return -1;
}
int count = size / sizeof(WALEntry);
wal->count = count;
wal->capacity = count;
wal->entries = malloc(count * sizeof(WALEntry));
if (wal->entries == NULL) {
file_close(handle);
return -1;
}
if (file_read_exact(handle, (char*) wal->entries, count * sizeof(WALEntry)) < 0) {
file_close(handle);
return -1;
}
return 0;
}
void wal_free(WAL *wal)
{
free(wal->entries);
file_close(wal->handle);
}
int wal_append(WAL *wal, WALEntry *entry)
{
if (wal->count == wal->capacity) {
int n = 2 * wal->capacity;
if (n < 8) n = 8;
void *p = realloc(wal->entries, n * sizeof(WALEntry));
if (p == NULL)
return -1;
wal->capacity = n;
wal->entries = p;
}
// TODO: Should truncate file on partial writes
if (file_write_exact(wal->handle, (char*) entry, sizeof(*entry)) < 0)
return -1;
if (file_sync(wal->handle) < 0)
return -1;
wal->entries[wal->count++] = *entry;
return 0;
}
int wal_truncate(WAL *wal, int new_count)
{
assert(new_count <= wal->count);
if (wal->count == new_count)
return 0;
if (file_truncate(wal->handle, new_count * sizeof(WALEntry)) < 0)
return -1;
if (file_set_offset(wal->handle, new_count * sizeof(WALEntry)) < 0)
return -1;
wal->count = new_count;
return 0;
}
uint64_t wal_last_term(WAL *wal)
{
if (wal->count == 0)
return 0;
return wal->entries[wal->count-1].term;
}
int wal_entry_count(WAL *wal)
{
return wal->count;
}
WALEntry *wal_peek_entry(WAL *wal, int idx)
{
assert(idx > -1);
assert(idx < wal->count);
return &wal->entries[idx];
}
void wal_replay_init(WALReplay *wal_replay, WAL *wal)
{
wal_replay->count = wal->count;
wal_replay->current = 0;
wal_replay->entries = wal->entries;
}
void wal_replay_free(WALReplay *wal_replay)
{
(void) wal_replay;
}
WALEntry *wal_replay_next(WALReplay *wal_replay)
{
if (wal_replay->current == wal_replay->count)
return NULL;
return &wal_replay->entries[wal_replay->current++];
}
+38
View File
@@ -0,0 +1,38 @@
#ifndef WAL_INCLUDED
#define WAL_INCLUDED
#include <lib/file_system.h>
#include <state_machine/state_machine.h>
typedef struct {
uint64_t term;
uint64_t client_id;
Operation oper;
} WALEntry;
typedef struct {
int count;
int capacity;
WALEntry *entries;
Handle handle;
} WAL;
typedef struct {
int count;
int current;
WALEntry *entries;
} WALReplay;
int wal_init(WAL *wal, string file);
void wal_free(WAL *wal);
int wal_append(WAL *wal, WALEntry *entry);
int wal_truncate(WAL *wal, int new_count);
uint64_t wal_last_term(WAL *wal);
int wal_entry_count(WAL *wal);
WALEntry *wal_peek_entry(WAL *wal, int idx);
void wal_replay_init(WALReplay *wal_replay, WAL *wal);
void wal_replay_free(WALReplay *wal_replay);
WALEntry *wal_replay_next(WALReplay *wal_replay);
#endif // WAL_INCLUDED