Background
Table of contents
Code
.
├── Makefile
├── lib/ # Infrastructure library (provided)
│ ├── rpc.c / rpc.h # RPC framework
│ ├── persister.c / persister.h # Crash-safe file persistence
│ ├── kv_types.c / kv_types.h # KV types (version_t, kv_err_t, kv_client_t)
│ ├── hash.c / hash.h # Pintos hash table (for your server state)
│ ├── list.c / list.h # Pintos intrusive list (used by hash)
│ └── cjson/ # JSON parser (for RPC bodies)
├── bin/
│ ├── kv_server.c # Single-server entrypoint (provided)
│ └── raft_server.c # KVRaft peer entrypoint (provided)
├── src/
│ ├── lock.c / lock.h # Part 2 & 4: Distributed lock
│ ├── kv_single/
│ │ ├── server.c / server.h # Part 1: KV server
│ │ └── client.c / client.h # Part 1: KV client
│ └── kv_raft/
│ ├── raft.c / raft.h # Part 3: Raft consensus
│ ├── rsm.c / rsm.h # Part 4: Replicated state machine
│ └── client.c / client.h # Part 4: Cluster client
└── tests/
├── kvsrv_test.c # Part 1-2 tests
├── raft_test.c # Part 3 tests
├── kvraft_test.c # Part 4 tests
├── network_proxy.c/.h # Test network proxy
└── test_common.c/.h # Test harness
Files you will modify
| File | Part | What to implement |
|---|---|---|
src/kv_single/server.c | 1 | KV server with versioned puts |
src/kv_single/client.c | 1 | Client with retry logic |
src/lock.c | 2, 4 | Distributed lock using KV |
src/kv_raft/raft.c | 3 | Raft consensus algorithm |
src/kv_raft/rsm.c | 4 | Replicated state machine |
src/kv_raft/client.c | 4 | Cluster-aware client |
Files you should read but not modify
| File | Description |
|---|---|
lib/rpc.h | RPC framework: rpc_dispatch_fn, rpc_client_t |
lib/kv_types.h | KV types (version_t, kv_err_t, kv_client_t) |
lib/persister.h | Crash-safe file persistence |
tests/test_common.*, tests/network_proxy.* | Test infrastructure |
RPC framework
Your servers and clients communicate over HTTP using a simple RPC framework defined in lib/rpc.h.
Server dispatch
Every server exposes a dispatch function of type:
typedef char *(*rpc_dispatch_fn)(void *ctx, const char *method,
const char *body, int client_id);
The framework routes POST /:method HTTP requests to dispatch(ctx, method, body, client_id). Your dispatch parses the JSON body, processes the request, and returns a malloc’d JSON response string (the framework frees it).
You start an RPC server with:
uint16_t port = rpc_server_start(ctx, dispatch);
RpcClient
Clients send RPCs using rpc_client_t:
rpc_client_t *rpc_client_new(const char *url, int client_id);
char *rpc_call(rpc_client_t *c, const char *method, const char *body);
char *rpc_call_timeout(rpc_client_t *c, const char *method,
const char *body, int timeout_sec);
rpc_call returns NULL when the network drops the request or the reply. Your client must handle this.
Defining RPCs
There is no .proto file or code generation. You define your own RPC types and serialize them to JSON with cJSON:
// Typed form on the stack (optional, for readability)
typedef struct {
char *key;
} get_args_t;
// Build the JSON on the wire
cJSON *j = cJSON_CreateObject();
cJSON_AddStringToObject(j, "key", key);
char *body = cJSON_PrintUnformatted(j);
cJSON_Delete(j);
// Send it
char *reply = rpc_call(endpoint, "get", body);
Your dispatch function pattern-matches on the method name and parses the body back:
char *kv_server_dispatch(void *ctx, const char *method,
const char *body, int client_id) {
if (strcmp(method, "get") == 0) {
cJSON *args = cJSON_Parse(body);
// ... build reply ...
cJSON_Delete(args);
return reply; // malloc'd
}
// ...
}
KV types
lib/kv_types.h provides shared types for the KV service:
version_t
typedef uint64_t version_t;
Each key has a version number. Version 0 means the key does not exist. The first successful put sets it to 1, and it increments on each subsequent successful put.
kv_err_t
typedef enum {
KV_OK = 0, // Operation succeeded
KV_NO_KEY, // Key does not exist
KV_VERSION, // Put's version didn't match server's version
KV_MAYBE, // Client retried; the put may or may not have applied
KV_WRONG_LEADER // Not the Raft leader; try another server
} kv_err_t;
Helpers kv_err_to_str and kv_err_from_str convert to/from the wire strings "OK", "NoKey", "Version", "Maybe", "WrongLeader".
kv_client_t vtable
typedef struct kv_client {
void *ctx;
void (*get)(void *ctx, const char *key,
char **out_value, version_t *out_version, kv_err_t *out_err);
kv_err_t (*put)(void *ctx, const char *key,
const char *value, version_t ver);
} kv_client_t;
Both the single-server client (Part 1) and the Raft client (Part 4) expose this vtable via a ..._as_kv_client helper. The lock_t (Part 2, 4) takes a kv_client_t *, so the same lock code works with either backend.
Persister
lib/persister.h provides crash-safe file persistence for Raft state:
typedef struct { char path[512]; } persister_t;
void persister_init(persister_t *p, const char *path);
void persister_save(persister_t *p, const char *data); // write-tmp + rename
char *persister_read(persister_t *p); // NULL if file missing; caller frees
You will use this in Part 3 to persist Raft state (term, vote, log) so that servers can recover after crashes.
Network simulation
The test harness interposes a network proxy between clients and servers. The proxy can:
- Drop requests: simulates request loss
- Drop replies: simulates reply loss (server processed the request, but client doesn’t know)
- Add delays: simulates network latency
- Block specific clients: simulates network partitions
Tests run in two network modes:
- Reliable (
network_config_reliable()): no drops or delay - Unreliable (
network_config_unreliable()): 50% drop rate for both requests and replies, plus small random delays
Your implementation must handle both correctly.